MDL-37717 warn teachers before disabling or deleting their enrolment method
[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     }
1042 /**
1043  * Is current user accessing course via this enrolment method?
1044  *
1045  * This is intended for operations that are going to affect enrol instances.
1046  *
1047  * @param stdClass $instance enrol instance
1048  * @return bool
1049  */
1050 function enrol_accessing_via_instance(stdClass $instance) {
1051     global $DB, $USER;
1053     if (empty($instance->id)) {
1054         return false;
1055     }
1057     if (is_siteadmin()) {
1058         // Admins may go anywhere.
1059         return false;
1060     }
1062     return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1066 /**
1067  * All enrol plugins should be based on this class,
1068  * this is also the main source of documentation.
1069  */
1070 abstract class enrol_plugin {
1071     protected $config = null;
1073     /**
1074      * Returns name of this enrol plugin
1075      * @return string
1076      */
1077     public function get_name() {
1078         // second word in class is always enrol name, sorry, no fancy plugin names with _
1079         $words = explode('_', get_class($this));
1080         return $words[1];
1081     }
1083     /**
1084      * Returns localised name of enrol instance
1085      *
1086      * @param object $instance (null is accepted too)
1087      * @return string
1088      */
1089     public function get_instance_name($instance) {
1090         if (empty($instance->name)) {
1091             $enrol = $this->get_name();
1092             return get_string('pluginname', 'enrol_'.$enrol);
1093         } else {
1094             $context = context_course::instance($instance->courseid);
1095             return format_string($instance->name, true, array('context'=>$context));
1096         }
1097     }
1099     /**
1100      * Returns optional enrolment information icons.
1101      *
1102      * This is used in course list for quick overview of enrolment options.
1103      *
1104      * We are not using single instance parameter because sometimes
1105      * we might want to prevent icon repetition when multiple instances
1106      * of one type exist. One instance may also produce several icons.
1107      *
1108      * @param array $instances all enrol instances of this type in one course
1109      * @return array of pix_icon
1110      */
1111     public function get_info_icons(array $instances) {
1112         return array();
1113     }
1115     /**
1116      * Returns optional enrolment instance description text.
1117      *
1118      * This is used in detailed course information.
1119      *
1120      *
1121      * @param object $instance
1122      * @return string short html text
1123      */
1124     public function get_description_text($instance) {
1125         return null;
1126     }
1128     /**
1129      * Makes sure config is loaded and cached.
1130      * @return void
1131      */
1132     protected function load_config() {
1133         if (!isset($this->config)) {
1134             $name = $this->get_name();
1135             $this->config = get_config("enrol_$name");
1136         }
1137     }
1139     /**
1140      * Returns plugin config value
1141      * @param  string $name
1142      * @param  string $default value if config does not exist yet
1143      * @return string value or default
1144      */
1145     public function get_config($name, $default = NULL) {
1146         $this->load_config();
1147         return isset($this->config->$name) ? $this->config->$name : $default;
1148     }
1150     /**
1151      * Sets plugin config value
1152      * @param  string $name name of config
1153      * @param  string $value string config value, null means delete
1154      * @return string value
1155      */
1156     public function set_config($name, $value) {
1157         $pluginname = $this->get_name();
1158         $this->load_config();
1159         if ($value === NULL) {
1160             unset($this->config->$name);
1161         } else {
1162             $this->config->$name = $value;
1163         }
1164         set_config($name, $value, "enrol_$pluginname");
1165     }
1167     /**
1168      * Does this plugin assign protected roles are can they be manually removed?
1169      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1170      */
1171     public function roles_protected() {
1172         return true;
1173     }
1175     /**
1176      * Does this plugin allow manual enrolments?
1177      *
1178      * @param stdClass $instance course enrol instance
1179      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1180      *
1181      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1182      */
1183     public function allow_enrol(stdClass $instance) {
1184         return false;
1185     }
1187     /**
1188      * Does this plugin allow manual unenrolment of all users?
1189      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1190      *
1191      * @param stdClass $instance course enrol instance
1192      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1193      */
1194     public function allow_unenrol(stdClass $instance) {
1195         return false;
1196     }
1198     /**
1199      * Does this plugin allow manual unenrolment of a specific user?
1200      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1201      *
1202      * This is useful especially for synchronisation plugins that
1203      * do suspend instead of full unenrolment.
1204      *
1205      * @param stdClass $instance course enrol instance
1206      * @param stdClass $ue record from user_enrolments table, specifies user
1207      *
1208      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1209      */
1210     public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1211         return $this->allow_unenrol($instance);
1212     }
1214     /**
1215      * Does this plugin allow manual changes in user_enrolments table?
1216      *
1217      * All plugins allowing this must implement 'enrol/xxx:manage' capability
1218      *
1219      * @param stdClass $instance course enrol instance
1220      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1221      */
1222     public function allow_manage(stdClass $instance) {
1223         return false;
1224     }
1226     /**
1227      * Does this plugin support some way to user to self enrol?
1228      *
1229      * @param stdClass $instance course enrol instance
1230      *
1231      * @return bool - true means show "Enrol me in this course" link in course UI
1232      */
1233     public function show_enrolme_link(stdClass $instance) {
1234         return false;
1235     }
1237     /**
1238      * Attempt to automatically enrol current user in course without any interaction,
1239      * calling code has to make sure the plugin and instance are active.
1240      *
1241      * This should return either a timestamp in the future or false.
1242      *
1243      * @param stdClass $instance course enrol instance
1244      * @return bool|int false means not enrolled, integer means timeend
1245      */
1246     public function try_autoenrol(stdClass $instance) {
1247         global $USER;
1249         return false;
1250     }
1252     /**
1253      * Attempt to automatically gain temporary guest access to course,
1254      * calling code has to make sure the plugin and instance are active.
1255      *
1256      * This should return either a timestamp in the future or false.
1257      *
1258      * @param stdClass $instance course enrol instance
1259      * @return bool|int false means no guest access, integer means timeend
1260      */
1261     public function try_guestaccess(stdClass $instance) {
1262         global $USER;
1264         return false;
1265     }
1267     /**
1268      * Enrol user into course via enrol instance.
1269      *
1270      * @param stdClass $instance
1271      * @param int $userid
1272      * @param int $roleid optional role id
1273      * @param int $timestart 0 means unknown
1274      * @param int $timeend 0 means forever
1275      * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
1276      * @param bool $recovergrades restore grade history
1277      * @return void
1278      */
1279     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
1280         global $DB, $USER, $CFG; // CFG necessary!!!
1282         if ($instance->courseid == SITEID) {
1283             throw new coding_exception('invalid attempt to enrol into frontpage course!');
1284         }
1286         $name = $this->get_name();
1287         $courseid = $instance->courseid;
1289         if ($instance->enrol !== $name) {
1290             throw new coding_exception('invalid enrol instance!');
1291         }
1292         $context = context_course::instance($instance->courseid, MUST_EXIST);
1293         if (!isset($recovergrades)) {
1294             $recovergrades = $CFG->recovergradesdefault;
1295         }
1297         $inserted = false;
1298         $updated  = false;
1299         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1300             //only update if timestart or timeend or status are different.
1301             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
1302                 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
1303             }
1304         } else {
1305             $ue = new stdClass();
1306             $ue->enrolid      = $instance->id;
1307             $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
1308             $ue->userid       = $userid;
1309             $ue->timestart    = $timestart;
1310             $ue->timeend      = $timeend;
1311             $ue->modifierid   = $USER->id;
1312             $ue->timecreated  = time();
1313             $ue->timemodified = $ue->timecreated;
1314             $ue->id = $DB->insert_record('user_enrolments', $ue);
1316             $inserted = true;
1317         }
1319         if ($inserted) {
1320             // Trigger event.
1321             $event = \core\event\user_enrolment_created::create(
1322                     array(
1323                         'objectid' => $ue->id,
1324                         'courseid' => $courseid,
1325                         'context' => $context,
1326                         'relateduserid' => $ue->userid,
1327                         'other' => array('enrol' => $name)
1328                         )
1329                     );
1330             $event->trigger();
1331         }
1333         if ($roleid) {
1334             // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
1335             if ($this->roles_protected()) {
1336                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1337             } else {
1338                 role_assign($roleid, $userid, $context->id);
1339             }
1340         }
1342         // Recover old grades if present.
1343         if ($recovergrades) {
1344             require_once("$CFG->libdir/gradelib.php");
1345             grade_recover_history_grades($userid, $courseid);
1346         }
1348         // reset current user enrolment caching
1349         if ($userid == $USER->id) {
1350             if (isset($USER->enrol['enrolled'][$courseid])) {
1351                 unset($USER->enrol['enrolled'][$courseid]);
1352             }
1353             if (isset($USER->enrol['tempguest'][$courseid])) {
1354                 unset($USER->enrol['tempguest'][$courseid]);
1355                 remove_temp_course_roles($context);
1356             }
1357         }
1358     }
1360     /**
1361      * Store user_enrolments changes and trigger event.
1362      *
1363      * @param stdClass $instance
1364      * @param int $userid
1365      * @param int $status
1366      * @param int $timestart
1367      * @param int $timeend
1368      * @return void
1369      */
1370     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1371         global $DB, $USER;
1373         $name = $this->get_name();
1375         if ($instance->enrol !== $name) {
1376             throw new coding_exception('invalid enrol instance!');
1377         }
1379         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1380             // weird, user not enrolled
1381             return;
1382         }
1384         $modified = false;
1385         if (isset($status) and $ue->status != $status) {
1386             $ue->status = $status;
1387             $modified = true;
1388         }
1389         if (isset($timestart) and $ue->timestart != $timestart) {
1390             $ue->timestart = $timestart;
1391             $modified = true;
1392         }
1393         if (isset($timeend) and $ue->timeend != $timeend) {
1394             $ue->timeend = $timeend;
1395             $modified = true;
1396         }
1398         if (!$modified) {
1399             // no change
1400             return;
1401         }
1403         $ue->modifierid = $USER->id;
1404         $DB->update_record('user_enrolments', $ue);
1405         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
1407         // Trigger event.
1408         $event = \core\event\user_enrolment_updated::create(
1409                 array(
1410                     'objectid' => $ue->id,
1411                     'courseid' => $instance->courseid,
1412                     'context' => context_course::instance($instance->courseid),
1413                     'relateduserid' => $ue->userid,
1414                     'other' => array('enrol' => $name)
1415                     )
1416                 );
1417         $event->trigger();
1418     }
1420     /**
1421      * Unenrol user from course,
1422      * the last unenrolment removes all remaining roles.
1423      *
1424      * @param stdClass $instance
1425      * @param int $userid
1426      * @return void
1427      */
1428     public function unenrol_user(stdClass $instance, $userid) {
1429         global $CFG, $USER, $DB;
1430         require_once("$CFG->dirroot/group/lib.php");
1432         $name = $this->get_name();
1433         $courseid = $instance->courseid;
1435         if ($instance->enrol !== $name) {
1436             throw new coding_exception('invalid enrol instance!');
1437         }
1438         $context = context_course::instance($instance->courseid, MUST_EXIST);
1440         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1441             // weird, user not enrolled
1442             return;
1443         }
1445         // Remove all users groups linked to this enrolment instance.
1446         if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
1447             foreach ($gms as $gm) {
1448                 groups_remove_member($gm->groupid, $gm->userid);
1449             }
1450         }
1452         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1453         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1455         // add extra info and trigger event
1456         $ue->courseid  = $courseid;
1457         $ue->enrol     = $name;
1459         $sql = "SELECT 'x'
1460                   FROM {user_enrolments} ue
1461                   JOIN {enrol} e ON (e.id = ue.enrolid)
1462                  WHERE ue.userid = :userid AND e.courseid = :courseid";
1463         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1464             $ue->lastenrol = false;
1466         } else {
1467             // the big cleanup IS necessary!
1468             require_once("$CFG->libdir/gradelib.php");
1470             // remove all remaining roles
1471             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1473             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1474             groups_delete_group_members($courseid, $userid);
1476             grade_user_unenrol($courseid, $userid);
1478             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1480             $ue->lastenrol = true; // means user not enrolled any more
1481         }
1482         // Trigger event.
1483         $event = \core\event\user_enrolment_deleted::create(
1484                 array(
1485                     'courseid' => $courseid,
1486                     'context' => $context,
1487                     'relateduserid' => $ue->userid,
1488                     'objectid' => $ue->id,
1489                     'other' => array(
1490                         'userenrolment' => (array)$ue,
1491                         'enrol' => $name
1492                         )
1493                     )
1494                 );
1495         $event->trigger();
1496         // reset all enrol caches
1497         $context->mark_dirty();
1499         // reset current user enrolment caching
1500         if ($userid == $USER->id) {
1501             if (isset($USER->enrol['enrolled'][$courseid])) {
1502                 unset($USER->enrol['enrolled'][$courseid]);
1503             }
1504             if (isset($USER->enrol['tempguest'][$courseid])) {
1505                 unset($USER->enrol['tempguest'][$courseid]);
1506                 remove_temp_course_roles($context);
1507             }
1508         }
1509     }
1511     /**
1512      * Forces synchronisation of user enrolments.
1513      *
1514      * This is important especially for external enrol plugins,
1515      * this function is called for all enabled enrol plugins
1516      * right after every user login.
1517      *
1518      * @param object $user user record
1519      * @return void
1520      */
1521     public function sync_user_enrolments($user) {
1522         // override if necessary
1523     }
1525     /**
1526      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1527      * @param int $courseid
1528      * @return moodle_url page url
1529      */
1530     public function get_newinstance_link($courseid) {
1531         // override for most plugins, check if instance already exists in cases only one instance is supported
1532         return NULL;
1533     }
1535     /**
1536      * Is it possible to delete enrol instance via standard UI?
1537      *
1538      * @param object $instance
1539      * @return bool
1540      */
1541     public function instance_deleteable($instance) {
1542         return true;
1543     }
1545     /**
1546      * Returns link to manual enrol UI if exists.
1547      * Does the access control tests automatically.
1548      *
1549      * @param object $instance
1550      * @return moodle_url
1551      */
1552     public function get_manual_enrol_link($instance) {
1553         return NULL;
1554     }
1556     /**
1557      * Returns list of unenrol links for all enrol instances in course.
1558      *
1559      * @param int $instance
1560      * @return moodle_url or NULL if self unenrolment not supported
1561      */
1562     public function get_unenrolself_link($instance) {
1563         global $USER, $CFG, $DB;
1565         $name = $this->get_name();
1566         if ($instance->enrol !== $name) {
1567             throw new coding_exception('invalid enrol instance!');
1568         }
1570         if ($instance->courseid == SITEID) {
1571             return NULL;
1572         }
1574         if (!enrol_is_enabled($name)) {
1575             return NULL;
1576         }
1578         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1579             return NULL;
1580         }
1582         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1583             return NULL;
1584         }
1586         $context = context_course::instance($instance->courseid, MUST_EXIST);
1588         if (!has_capability("enrol/$name:unenrolself", $context)) {
1589             return NULL;
1590         }
1592         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1593             return NULL;
1594         }
1596         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
1597     }
1599     /**
1600      * Adds enrol instance UI to course edit form
1601      *
1602      * @param object $instance enrol instance or null if does not exist yet
1603      * @param MoodleQuickForm $mform
1604      * @param object $data
1605      * @param object $context context of existing course or parent category if course does not exist
1606      * @return void
1607      */
1608     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1609         // override - usually at least enable/disable switch, has to add own form header
1610     }
1612     /**
1613      * Validates course edit form data
1614      *
1615      * @param object $instance enrol instance or null if does not exist yet
1616      * @param array $data
1617      * @param object $context context of existing course or parent category if course does not exist
1618      * @return array errors array
1619      */
1620     public function course_edit_validation($instance, array $data, $context) {
1621         return array();
1622     }
1624     /**
1625      * Called after updating/inserting course.
1626      *
1627      * @param bool $inserted true if course just inserted
1628      * @param object $course
1629      * @param object $data form data
1630      * @return void
1631      */
1632     public function course_updated($inserted, $course, $data) {
1633         if ($inserted) {
1634             if ($this->get_config('defaultenrol')) {
1635                 $this->add_default_instance($course);
1636             }
1637         }
1638     }
1640     /**
1641      * Add new instance of enrol plugin.
1642      * @param object $course
1643      * @param array instance fields
1644      * @return int id of new instance, null if can not be created
1645      */
1646     public function add_instance($course, array $fields = NULL) {
1647         global $DB;
1649         if ($course->id == SITEID) {
1650             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1651         }
1653         $instance = new stdClass();
1654         $instance->enrol          = $this->get_name();
1655         $instance->status         = ENROL_INSTANCE_ENABLED;
1656         $instance->courseid       = $course->id;
1657         $instance->enrolstartdate = 0;
1658         $instance->enrolenddate   = 0;
1659         $instance->timemodified   = time();
1660         $instance->timecreated    = $instance->timemodified;
1661         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1663         $fields = (array)$fields;
1664         unset($fields['enrol']);
1665         unset($fields['courseid']);
1666         unset($fields['sortorder']);
1667         foreach($fields as $field=>$value) {
1668             $instance->$field = $value;
1669         }
1671         return $DB->insert_record('enrol', $instance);
1672     }
1674     /**
1675      * Add new instance of enrol plugin with default settings,
1676      * called when adding new instance manually or when adding new course.
1677      *
1678      * Not all plugins support this.
1679      *
1680      * @param object $course
1681      * @return int id of new instance or null if no default supported
1682      */
1683     public function add_default_instance($course) {
1684         return null;
1685     }
1687     /**
1688      * Update instance status
1689      *
1690      * Override when plugin needs to do some action when enabled or disabled.
1691      *
1692      * @param stdClass $instance
1693      * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
1694      * @return void
1695      */
1696     public function update_status($instance, $newstatus) {
1697         global $DB;
1699         $instance->status = $newstatus;
1700         $DB->update_record('enrol', $instance);
1702         // invalidate all enrol caches
1703         $context = context_course::instance($instance->courseid);
1704         $context->mark_dirty();
1705     }
1707     /**
1708      * Delete course enrol plugin instance, unenrol all users.
1709      * @param object $instance
1710      * @return void
1711      */
1712     public function delete_instance($instance) {
1713         global $DB;
1715         $name = $this->get_name();
1716         if ($instance->enrol !== $name) {
1717             throw new coding_exception('invalid enrol instance!');
1718         }
1720         //first unenrol all users
1721         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1722         foreach ($participants as $participant) {
1723             $this->unenrol_user($instance, $participant->userid);
1724         }
1725         $participants->close();
1727         // now clean up all remainders that were not removed correctly
1728         $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1729         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1730         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1732         // finally drop the enrol row
1733         $DB->delete_records('enrol', array('id'=>$instance->id));
1735         // invalidate all enrol caches
1736         $context = context_course::instance($instance->courseid);
1737         $context->mark_dirty();
1738     }
1740     /**
1741      * Creates course enrol form, checks if form submitted
1742      * and enrols user if necessary. It can also redirect.
1743      *
1744      * @param stdClass $instance
1745      * @return string html text, usually a form in a text box
1746      */
1747     public function enrol_page_hook(stdClass $instance) {
1748         return null;
1749     }
1751     /**
1752      * Checks if user can self enrol.
1753      *
1754      * @param stdClass $instance enrolment instance
1755      * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
1756      *             used by navigation to improve performance.
1757      * @return bool|string true if successful, else error message or false
1758      */
1759     public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
1760         return false;
1761     }
1763     /**
1764      * Return information for enrolment instance containing list of parameters required
1765      * for enrolment, name of enrolment plugin etc.
1766      *
1767      * @param stdClass $instance enrolment instance
1768      * @return array instance info.
1769      */
1770     public function get_enrol_info(stdClass $instance) {
1771         return null;
1772     }
1774     /**
1775      * Adds navigation links into course admin block.
1776      *
1777      * By defaults looks for manage links only.
1778      *
1779      * @param navigation_node $instancesnode
1780      * @param stdClass $instance
1781      * @return void
1782      */
1783     public function add_course_navigation($instancesnode, stdClass $instance) {
1784         // usually adds manage users
1785     }
1787     /**
1788      * Returns edit icons for the page with list of instances
1789      * @param stdClass $instance
1790      * @return array
1791      */
1792     public function get_action_icons(stdClass $instance) {
1793         return array();
1794     }
1796     /**
1797      * Reads version.php and determines if it is necessary
1798      * to execute the cron job now.
1799      * @return bool
1800      */
1801     public function is_cron_required() {
1802         global $CFG;
1804         $name = $this->get_name();
1805         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1806         $plugin = new stdClass();
1807         include($versionfile);
1808         if (empty($plugin->cron)) {
1809             return false;
1810         }
1811         $lastexecuted = $this->get_config('lastcron', 0);
1812         if ($lastexecuted + $plugin->cron < time()) {
1813             return true;
1814         } else {
1815             return false;
1816         }
1817     }
1819     /**
1820      * Called for all enabled enrol plugins that returned true from is_cron_required().
1821      * @return void
1822      */
1823     public function cron() {
1824     }
1826     /**
1827      * Called when user is about to be deleted
1828      * @param object $user
1829      * @return void
1830      */
1831     public function user_delete($user) {
1832         global $DB;
1834         $sql = "SELECT e.*
1835                   FROM {enrol} e
1836                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
1837                  WHERE e.enrol = :name AND ue.userid = :userid";
1838         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1840         $rs = $DB->get_recordset_sql($sql, $params);
1841         foreach($rs as $instance) {
1842             $this->unenrol_user($instance, $user->id);
1843         }
1844         $rs->close();
1845     }
1847     /**
1848      * Returns an enrol_user_button that takes the user to a page where they are able to
1849      * enrol users into the managers course through this plugin.
1850      *
1851      * Optional: If the plugin supports manual enrolments it can choose to override this
1852      * otherwise it shouldn't
1853      *
1854      * @param course_enrolment_manager $manager
1855      * @return enrol_user_button|false
1856      */
1857     public function get_manual_enrol_button(course_enrolment_manager $manager) {
1858         return false;
1859     }
1861     /**
1862      * Gets an array of the user enrolment actions
1863      *
1864      * @param course_enrolment_manager $manager
1865      * @param stdClass $ue
1866      * @return array An array of user_enrolment_actions
1867      */
1868     public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
1869         return array();
1870     }
1872     /**
1873      * Returns true if the plugin has one or more bulk operations that can be performed on
1874      * user enrolments.
1875      *
1876      * @param course_enrolment_manager $manager
1877      * @return bool
1878      */
1879     public function has_bulk_operations(course_enrolment_manager $manager) {
1880        return false;
1881     }
1883     /**
1884      * Return an array of enrol_bulk_enrolment_operation objects that define
1885      * the bulk actions that can be performed on user enrolments by the plugin.
1886      *
1887      * @param course_enrolment_manager $manager
1888      * @return array
1889      */
1890     public function get_bulk_operations(course_enrolment_manager $manager) {
1891         return array();
1892     }
1894     /**
1895      * Do any enrolments need expiration processing.
1896      *
1897      * Plugins that want to call this functionality must implement 'expiredaction' config setting.
1898      *
1899      * @param progress_trace $trace
1900      * @param int $courseid one course, empty mean all
1901      * @return bool true if any data processed, false if not
1902      */
1903     public function process_expirations(progress_trace $trace, $courseid = null) {
1904         global $DB;
1906         $name = $this->get_name();
1907         if (!enrol_is_enabled($name)) {
1908             $trace->finished();
1909             return false;
1910         }
1912         $processed = false;
1913         $params = array();
1914         $coursesql = "";
1915         if ($courseid) {
1916             $coursesql = "AND e.courseid = :courseid";
1917         }
1919         // Deal with expired accounts.
1920         $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
1922         if ($action == ENROL_EXT_REMOVED_UNENROL) {
1923             $instances = array();
1924             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
1925                       FROM {user_enrolments} ue
1926                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
1927                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
1928                      WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
1929             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
1931             $rs = $DB->get_recordset_sql($sql, $params);
1932             foreach ($rs as $ue) {
1933                 if (!$processed) {
1934                     $trace->output("Starting processing of enrol_$name expirations...");
1935                     $processed = true;
1936                 }
1937                 if (empty($instances[$ue->enrolid])) {
1938                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
1939                 }
1940                 $instance = $instances[$ue->enrolid];
1941                 if (!$this->roles_protected()) {
1942                     // Let's just guess what extra roles are supposed to be removed.
1943                     if ($instance->roleid) {
1944                         role_unassign($instance->roleid, $ue->userid, $ue->contextid);
1945                     }
1946                 }
1947                 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
1948                 $this->unenrol_user($instance, $ue->userid);
1949                 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
1950             }
1951             $rs->close();
1952             unset($instances);
1954         } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
1955             $instances = array();
1956             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
1957                       FROM {user_enrolments} ue
1958                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
1959                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
1960                      WHERE ue.timeend > 0 AND ue.timeend < :now
1961                            AND ue.status = :useractive $coursesql";
1962             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
1963             $rs = $DB->get_recordset_sql($sql, $params);
1964             foreach ($rs as $ue) {
1965                 if (!$processed) {
1966                     $trace->output("Starting processing of enrol_$name expirations...");
1967                     $processed = true;
1968                 }
1969                 if (empty($instances[$ue->enrolid])) {
1970                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
1971                 }
1972                 $instance = $instances[$ue->enrolid];
1974                 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
1975                     if (!$this->roles_protected()) {
1976                         // Let's just guess what roles should be removed.
1977                         $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
1978                         if ($count == 1) {
1979                             role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
1981                         } else if ($count > 1 and $instance->roleid) {
1982                             role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
1983                         }
1984                     }
1985                     // In any case remove all roles that belong to this instance and user.
1986                     role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
1987                     // Final cleanup of subcontexts if there are no more course roles.
1988                     if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
1989                         role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
1990                     }
1991                 }
1993                 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
1994                 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
1995             }
1996             $rs->close();
1997             unset($instances);
1999         } else {
2000             // ENROL_EXT_REMOVED_KEEP means no changes.
2001         }
2003         if ($processed) {
2004             $trace->output("...finished processing of enrol_$name expirations");
2005         } else {
2006             $trace->output("No expired enrol_$name enrolments detected");
2007         }
2008         $trace->finished();
2010         return $processed;
2011     }
2013     /**
2014      * Send expiry notifications.
2015      *
2016      * Plugin that wants to have expiry notification MUST implement following:
2017      * - expirynotifyhour plugin setting,
2018      * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
2019      * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
2020      *   expirymessageenrolledsubject and expirymessageenrolledbody),
2021      * - expiry_notification provider in db/messages.php,
2022      * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2023      * - something that calls this method, such as cron.
2024      *
2025      * @param progress_trace $trace (accepts bool for backwards compatibility only)
2026      */
2027     public function send_expiry_notifications($trace) {
2028         global $DB, $CFG;
2030         $name = $this->get_name();
2031         if (!enrol_is_enabled($name)) {
2032             $trace->finished();
2033             return;
2034         }
2036         // Unfortunately this may take a long time, it should not be interrupted,
2037         // otherwise users get duplicate notification.
2039         @set_time_limit(0);
2040         raise_memory_limit(MEMORY_HUGE);
2043         $expirynotifylast = $this->get_config('expirynotifylast', 0);
2044         $expirynotifyhour = $this->get_config('expirynotifyhour');
2045         if (is_null($expirynotifyhour)) {
2046             debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
2047             $trace->finished();
2048             return;
2049         }
2051         if (!($trace instanceof progress_trace)) {
2052             $trace = $trace ? new text_progress_trace() : new null_progress_trace();
2053             debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
2054         }
2056         $timenow = time();
2057         $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
2059         if ($expirynotifylast > $notifytime) {
2060             $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
2061             $trace->finished();
2062             return;
2064         } else if ($timenow < $notifytime) {
2065             $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
2066             $trace->finished();
2067             return;
2068         }
2070         $trace->output('Processing '.$name.' enrolment expiration notifications...');
2072         // Notify users responsible for enrolment once every day.
2073         $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
2074                   FROM {user_enrolments} ue
2075                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
2076                   JOIN {course} c ON (c.id = e.courseid)
2077                   JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
2078                  WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
2079               ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
2080         $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
2082         $rs = $DB->get_recordset_sql($sql, $params);
2084         $lastenrollid = 0;
2085         $users = array();
2087         foreach($rs as $ue) {
2088             if ($lastenrollid and $lastenrollid != $ue->enrolid) {
2089                 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2090                 $users = array();
2091             }
2092             $lastenrollid = $ue->enrolid;
2094             $enroller = $this->get_enroller($ue->enrolid);
2095             $context = context_course::instance($ue->courseid);
2097             $user = $DB->get_record('user', array('id'=>$ue->userid));
2099             $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
2101             if (!$ue->notifyall) {
2102                 continue;
2103             }
2105             if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
2106                 // Notify enrolled users only once at the start of the threshold.
2107                 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2108                 continue;
2109             }
2111             $this->notify_expiry_enrolled($user, $ue, $trace);
2112         }
2113         $rs->close();
2115         if ($lastenrollid and $users) {
2116             $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2117         }
2119         $trace->output('...notification processing finished.');
2120         $trace->finished();
2122         $this->set_config('expirynotifylast', $timenow);
2123     }
2125     /**
2126      * Returns the user who is responsible for enrolments for given instance.
2127      *
2128      * Override if plugin knows anybody better than admin.
2129      *
2130      * @param int $instanceid enrolment instance id
2131      * @return stdClass user record
2132      */
2133     protected function get_enroller($instanceid) {
2134         return get_admin();
2135     }
2137     /**
2138      * Notify user about incoming expiration of their enrolment,
2139      * it is called only if notification of enrolled users (aka students) is enabled in course.
2140      *
2141      * This is executed only once for each expiring enrolment right
2142      * at the start of the expiration threshold.
2143      *
2144      * @param stdClass $user
2145      * @param stdClass $ue
2146      * @param progress_trace $trace
2147      */
2148     protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
2149         global $CFG, $SESSION;
2151         $name = $this->get_name();
2153         // Some nasty hackery to get strings and dates localised for target user.
2154         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
2155         if (get_string_manager()->translation_exists($user->lang, false)) {
2156             $SESSION->lang = $user->lang;
2157             moodle_setlocale();
2158         }
2160         $enroller = $this->get_enroller($ue->enrolid);
2161         $context = context_course::instance($ue->courseid);
2163         $a = new stdClass();
2164         $a->course   = format_string($ue->fullname, true, array('context'=>$context));
2165         $a->user     = fullname($user, true);
2166         $a->timeend  = userdate($ue->timeend, '', $user->timezone);
2167         $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
2169         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
2170         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
2172         $message = new stdClass();
2173         $message->notification      = 1;
2174         $message->component         = 'enrol_'.$name;
2175         $message->name              = 'expiry_notification';
2176         $message->userfrom          = $enroller;
2177         $message->userto            = $user;
2178         $message->subject           = $subject;
2179         $message->fullmessage       = $body;
2180         $message->fullmessageformat = FORMAT_MARKDOWN;
2181         $message->fullmessagehtml   = markdown_to_html($body);
2182         $message->smallmessage      = $subject;
2183         $message->contexturlname    = $a->course;
2184         $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
2186         if (message_send($message)) {
2187             $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2188         } else {
2189             $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2190         }
2192         if ($SESSION->lang !== $sessionlang) {
2193             $SESSION->lang = $sessionlang;
2194             moodle_setlocale();
2195         }
2196     }
2198     /**
2199      * Notify person responsible for enrolments that some user enrolments will be expired soon,
2200      * it is called only if notification of enrollers (aka teachers) is enabled in course.
2201      *
2202      * This is called repeatedly every day for each course if there are any pending expiration
2203      * in the expiration threshold.
2204      *
2205      * @param int $eid
2206      * @param array $users
2207      * @param progress_trace $trace
2208      */
2209     protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
2210         global $DB, $SESSION;
2212         $name = $this->get_name();
2214         $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
2215         $context = context_course::instance($instance->courseid);
2216         $course = $DB->get_record('course', array('id'=>$instance->courseid));
2218         $enroller = $this->get_enroller($instance->id);
2219         $admin = get_admin();
2221         // Some nasty hackery to get strings and dates localised for target user.
2222         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
2223         if (get_string_manager()->translation_exists($enroller->lang, false)) {
2224             $SESSION->lang = $enroller->lang;
2225             moodle_setlocale();
2226         }
2228         foreach($users as $key=>$info) {
2229             $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
2230         }
2232         $a = new stdClass();
2233         $a->course    = format_string($course->fullname, true, array('context'=>$context));
2234         $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
2235         $a->users     = implode("\n", $users);
2236         $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid));
2238         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
2239         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
2241         $message = new stdClass();
2242         $message->notification      = 1;
2243         $message->component         = 'enrol_'.$name;
2244         $message->name              = 'expiry_notification';
2245         $message->userfrom          = $admin;
2246         $message->userto            = $enroller;
2247         $message->subject           = $subject;
2248         $message->fullmessage       = $body;
2249         $message->fullmessageformat = FORMAT_MARKDOWN;
2250         $message->fullmessagehtml   = markdown_to_html($body);
2251         $message->smallmessage      = $subject;
2252         $message->contexturlname    = $a->course;
2253         $message->contexturl        = $a->extendurl;
2255         if (message_send($message)) {
2256             $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2257         } else {
2258             $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2259         }
2261         if ($SESSION->lang !== $sessionlang) {
2262             $SESSION->lang = $sessionlang;
2263             moodle_setlocale();
2264         }
2265     }
2267     /**
2268      * Automatic enrol sync executed during restore.
2269      * Useful for automatic sync by course->idnumber or course category.
2270      * @param stdClass $course course record
2271      */
2272     public function restore_sync_course($course) {
2273         // Override if necessary.
2274     }
2276     /**
2277      * Restore instance and map settings.
2278      *
2279      * @param restore_enrolments_structure_step $step
2280      * @param stdClass $data
2281      * @param stdClass $course
2282      * @param int $oldid
2283      */
2284     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
2285         // Do not call this from overridden methods, restore and set new id there.
2286         $step->set_mapping('enrol', $oldid, 0);
2287     }
2289     /**
2290      * Restore user enrolment.
2291      *
2292      * @param restore_enrolments_structure_step $step
2293      * @param stdClass $data
2294      * @param stdClass $instance
2295      * @param int $oldinstancestatus
2296      * @param int $userid
2297      */
2298     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
2299         // Override as necessary if plugin supports restore of enrolments.
2300     }
2302     /**
2303      * Restore role assignment.
2304      *
2305      * @param stdClass $instance
2306      * @param int $roleid
2307      * @param int $userid
2308      * @param int $contextid
2309      */
2310     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
2311         // No role assignment by default, override if necessary.
2312     }
2314     /**
2315      * Restore user group membership.
2316      * @param stdClass $instance
2317      * @param int $groupid
2318      * @param int $userid
2319      */
2320     public function restore_group_member($instance, $groupid, $userid) {
2321         // Implement if you want to restore protected group memberships,
2322         // usually this is not necessary because plugins should be able to recreate the memberships automatically.
2323     }