fbb5c3975c9fc17508972290ce908ad32fcc6cbd
[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  * Do not send email.
72  */
73 define('ENROL_DO_NOT_SEND_EMAIL', 0);
75 /**
76  * Send email from course contact.
77  */
78 define('ENROL_SEND_EMAIL_FROM_COURSE_CONTACT', 1);
80 /**
81  * Send email from enrolment key holder.
82  */
83 define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2);
85 /**
86  * Send email from no reply address.
87  */
88 define('ENROL_SEND_EMAIL_FROM_NOREPLY', 3);
90 /** Edit enrolment action. */
91 define('ENROL_ACTION_EDIT', 'editenrolment');
93 /** Unenrol action. */
94 define('ENROL_ACTION_UNENROL', 'unenrol');
96 /**
97  * Returns instances of enrol plugins
98  * @param bool $enabled return enabled only
99  * @return array of enrol plugins name=>instance
100  */
101 function enrol_get_plugins($enabled) {
102     global $CFG;
104     $result = array();
106     if ($enabled) {
107         // sorted by enabled plugin order
108         $enabled = explode(',', $CFG->enrol_plugins_enabled);
109         $plugins = array();
110         foreach ($enabled as $plugin) {
111             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
112         }
113     } else {
114         // sorted alphabetically
115         $plugins = core_component::get_plugin_list('enrol');
116         ksort($plugins);
117     }
119     foreach ($plugins as $plugin=>$location) {
120         $class = "enrol_{$plugin}_plugin";
121         if (!class_exists($class)) {
122             if (!file_exists("$location/lib.php")) {
123                 continue;
124             }
125             include_once("$location/lib.php");
126             if (!class_exists($class)) {
127                 continue;
128             }
129         }
131         $result[$plugin] = new $class();
132     }
134     return $result;
137 /**
138  * Returns instance of enrol plugin
139  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
140  * @return enrol_plugin
141  */
142 function enrol_get_plugin($name) {
143     global $CFG;
145     $name = clean_param($name, PARAM_PLUGIN);
147     if (empty($name)) {
148         // ignore malformed or missing plugin names completely
149         return null;
150     }
152     $location = "$CFG->dirroot/enrol/$name";
154     $class = "enrol_{$name}_plugin";
155     if (!class_exists($class)) {
156         if (!file_exists("$location/lib.php")) {
157             return null;
158         }
159         include_once("$location/lib.php");
160         if (!class_exists($class)) {
161             return null;
162         }
163     }
165     return new $class();
168 /**
169  * Returns enrolment instances in given course.
170  * @param int $courseid
171  * @param bool $enabled
172  * @return array of enrol instances
173  */
174 function enrol_get_instances($courseid, $enabled) {
175     global $DB, $CFG;
177     if (!$enabled) {
178         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
179     }
181     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
183     $enabled = explode(',', $CFG->enrol_plugins_enabled);
184     foreach ($result as $key=>$instance) {
185         if (!in_array($instance->enrol, $enabled)) {
186             unset($result[$key]);
187             continue;
188         }
189         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
190             // broken plugin
191             unset($result[$key]);
192             continue;
193         }
194     }
196     return $result;
199 /**
200  * Checks if a given plugin is in the list of enabled enrolment plugins.
201  *
202  * @param string $enrol Enrolment plugin name
203  * @return boolean Whether the plugin is enabled
204  */
205 function enrol_is_enabled($enrol) {
206     global $CFG;
208     if (empty($CFG->enrol_plugins_enabled)) {
209         return false;
210     }
211     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
214 /**
215  * Check all the login enrolment information for the given user object
216  * by querying the enrolment plugins
217  *
218  * This function may be very slow, use only once after log-in or login-as.
219  *
220  * @param stdClass $user
221  * @return void
222  */
223 function enrol_check_plugins($user) {
224     global $CFG;
226     if (empty($user->id) or isguestuser($user)) {
227         // shortcut - there is no enrolment work for guests and not-logged-in users
228         return;
229     }
231     // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
232     // which proved it was actually not necessary.
234     static $inprogress = array();  // To prevent this function being called more than once in an invocation
236     if (!empty($inprogress[$user->id])) {
237         return;
238     }
240     $inprogress[$user->id] = true;  // Set the flag
242     $enabled = enrol_get_plugins(true);
244     foreach($enabled as $enrol) {
245         $enrol->sync_user_enrolments($user);
246     }
248     unset($inprogress[$user->id]);  // Unset the flag
251 /**
252  * Do these two students share any course?
253  *
254  * The courses has to be visible and enrolments has to be active,
255  * timestart and timeend restrictions are ignored.
256  *
257  * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
258  * to true.
259  *
260  * @param stdClass|int $user1
261  * @param stdClass|int $user2
262  * @return bool
263  */
264 function enrol_sharing_course($user1, $user2) {
265     return enrol_get_shared_courses($user1, $user2, false, true);
268 /**
269  * Returns any courses shared by the two users
270  *
271  * The courses has to be visible and enrolments has to be active,
272  * timestart and timeend restrictions are ignored.
273  *
274  * @global moodle_database $DB
275  * @param stdClass|int $user1
276  * @param stdClass|int $user2
277  * @param bool $preloadcontexts If set to true contexts for the returned courses
278  *              will be preloaded.
279  * @param bool $checkexistsonly If set to true then this function will return true
280  *              if the users share any courses and false if not.
281  * @return array|bool An array of courses that both users are enrolled in OR if
282  *              $checkexistsonly set returns true if the users share any courses
283  *              and false if not.
284  */
285 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
286     global $DB, $CFG;
288     $user1 = isset($user1->id) ? $user1->id : $user1;
289     $user2 = isset($user2->id) ? $user2->id : $user2;
291     if (empty($user1) or empty($user2)) {
292         return false;
293     }
295     if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
296         return false;
297     }
299     list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
300     list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
301     $params = array_merge($params1, $params2);
302     $params['enabled1'] = ENROL_INSTANCE_ENABLED;
303     $params['enabled2'] = ENROL_INSTANCE_ENABLED;
304     $params['active1'] = ENROL_USER_ACTIVE;
305     $params['active2'] = ENROL_USER_ACTIVE;
306     $params['user1']   = $user1;
307     $params['user2']   = $user2;
309     $ctxselect = '';
310     $ctxjoin = '';
311     if ($preloadcontexts) {
312         $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
313         $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
314         $params['contextlevel'] = CONTEXT_COURSE;
315     }
317     $sql = "SELECT c.* $ctxselect
318               FROM {course} c
319               JOIN (
320                 SELECT DISTINCT c.id
321                   FROM {course} c
322                   JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
323                   JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
324                   JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
325                   JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
326                  WHERE c.visible = 1
327               ) ec ON ec.id = c.id
328               $ctxjoin";
330     if ($checkexistsonly) {
331         return $DB->record_exists_sql($sql, $params);
332     } else {
333         $courses = $DB->get_records_sql($sql, $params);
334         if ($preloadcontexts) {
335             array_map('context_helper::preload_from_record', $courses);
336         }
337         return $courses;
338     }
341 /**
342  * This function adds necessary enrol plugins UI into the course edit form.
343  *
344  * @param MoodleQuickForm $mform
345  * @param object $data course edit form data
346  * @param object $context context of existing course or parent category if course does not exist
347  * @return void
348  */
349 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
350     $plugins = enrol_get_plugins(true);
351     if (!empty($data->id)) {
352         $instances = enrol_get_instances($data->id, false);
353         foreach ($instances as $instance) {
354             if (!isset($plugins[$instance->enrol])) {
355                 continue;
356             }
357             $plugin = $plugins[$instance->enrol];
358             $plugin->course_edit_form($instance, $mform, $data, $context);
359         }
360     } else {
361         foreach ($plugins as $plugin) {
362             $plugin->course_edit_form(NULL, $mform, $data, $context);
363         }
364     }
367 /**
368  * Validate course edit form data
369  *
370  * @param array $data raw form data
371  * @param object $context context of existing course or parent category if course does not exist
372  * @return array errors array
373  */
374 function enrol_course_edit_validation(array $data, $context) {
375     $errors = array();
376     $plugins = enrol_get_plugins(true);
378     if (!empty($data['id'])) {
379         $instances = enrol_get_instances($data['id'], false);
380         foreach ($instances as $instance) {
381             if (!isset($plugins[$instance->enrol])) {
382                 continue;
383             }
384             $plugin = $plugins[$instance->enrol];
385             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
386         }
387     } else {
388         foreach ($plugins as $plugin) {
389             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
390         }
391     }
393     return $errors;
396 /**
397  * Update enrol instances after course edit form submission
398  * @param bool $inserted true means new course added, false course already existed
399  * @param object $course
400  * @param object $data form data
401  * @return void
402  */
403 function enrol_course_updated($inserted, $course, $data) {
404     global $DB, $CFG;
406     $plugins = enrol_get_plugins(true);
408     foreach ($plugins as $plugin) {
409         $plugin->course_updated($inserted, $course, $data);
410     }
413 /**
414  * Add navigation nodes
415  * @param navigation_node $coursenode
416  * @param object $course
417  * @return void
418  */
419 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
420     global $CFG;
422     $coursecontext = context_course::instance($course->id);
424     $instances = enrol_get_instances($course->id, true);
425     $plugins   = enrol_get_plugins(true);
427     // we do not want to break all course pages if there is some borked enrol plugin, right?
428     foreach ($instances as $k=>$instance) {
429         if (!isset($plugins[$instance->enrol])) {
430             unset($instances[$k]);
431         }
432     }
434     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
436     if ($course->id != SITEID) {
437         // list all participants - allows assigning roles, groups, etc.
438         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
439             $url = new moodle_url('/user/index.php', array('id'=>$course->id));
440             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
441         }
443         // manage enrol plugin instances
444         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
445             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
446         } else {
447             $url = NULL;
448         }
449         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
451         // each instance decides how to configure itself or how many other nav items are exposed
452         foreach ($instances as $instance) {
453             if (!isset($plugins[$instance->enrol])) {
454                 continue;
455             }
456             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
457         }
459         if (!$url) {
460             $instancesnode->trim_if_empty();
461         }
462     }
464     // Manage groups in this course or even frontpage
465     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
466         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
467         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
468     }
470      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
471         // Override roles
472         if (has_capability('moodle/role:review', $coursecontext)) {
473             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
474         } else {
475             $url = NULL;
476         }
477         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
479         // Add assign or override roles if allowed
480         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
481             if (has_capability('moodle/role:assign', $coursecontext)) {
482                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
483                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
484             }
485         }
486         // Check role permissions
487         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
488             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
489             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
490         }
491      }
493      // Deal somehow with users that are not enrolled but still got a role somehow
494     if ($course->id != SITEID) {
495         //TODO, create some new UI for role assignments at course level
496         if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
497             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
498             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
499         }
500     }
502     // just in case nothing was actually added
503     $usersnode->trim_if_empty();
505     if ($course->id != SITEID) {
506         if (isguestuser() or !isloggedin()) {
507             // guest account can not be enrolled - no links for them
508         } else if (is_enrolled($coursecontext)) {
509             // unenrol link if possible
510             foreach ($instances as $instance) {
511                 if (!isset($plugins[$instance->enrol])) {
512                     continue;
513                 }
514                 $plugin = $plugins[$instance->enrol];
515                 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
516                     $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
517                     $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
518                     break;
519                     //TODO. deal with multiple unenrol links - not likely case, but still...
520                 }
521             }
522         } else {
523             // enrol link if possible
524             if (is_viewing($coursecontext)) {
525                 // better not show any enrol link, this is intended for managers and inspectors
526             } else {
527                 foreach ($instances as $instance) {
528                     if (!isset($plugins[$instance->enrol])) {
529                         continue;
530                     }
531                     $plugin = $plugins[$instance->enrol];
532                     if ($plugin->show_enrolme_link($instance)) {
533                         $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
534                         $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
535                         $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
536                         break;
537                     }
538                 }
539             }
540         }
541     }
544 /**
545  * Returns list of courses current $USER is enrolled in and can access
546  *
547  * The $fields param is a list of field names to ADD so name just the fields you really need,
548  * which will be added and uniq'd.
549  *
550  * If $allaccessible is true, this will additionally return courses that the current user is not
551  * enrolled in, but can access because they are open to the user for other reasons (course view
552  * permission, currently viewing course as a guest, or course allows guest access without
553  * password).
554  *
555  * @param string|array $fields Extra fields to be returned (array or comma-separated list).
556  * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
557  * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
558  * "ue" for the user_enrolments table.
559  * @param int $limit max number of courses
560  * @param array $courseids the list of course ids to filter by
561  * @param bool $allaccessible Include courses user is not enrolled in, but can access
562  * @param int $offset Offset the result set by this number
563  * @param array $excludecourses IDs of hidden courses to exclude from search
564  * @return array
565  */
566 function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
567     $offset = 0, $excludecourses = []) {
568     global $DB, $USER, $CFG;
570     // Re-Arrange the course sorting according to the admin settings.
571     $sort = enrol_get_courses_sortingsql($sort);
573     // Guest account does not have any enrolled courses.
574     if (!$allaccessible && (isguestuser() or !isloggedin())) {
575         return array();
576     }
578     $basefields = array('id', 'category', 'sortorder',
579                         'shortname', 'fullname', 'idnumber',
580                         'startdate', 'visible',
581                         'groupmode', 'groupmodeforce', 'cacherev');
583     if (empty($fields)) {
584         $fields = $basefields;
585     } else if (is_string($fields)) {
586         // turn the fields from a string to an array
587         $fields = explode(',', $fields);
588         $fields = array_map('trim', $fields);
589         $fields = array_unique(array_merge($basefields, $fields));
590     } else if (is_array($fields)) {
591         $fields = array_unique(array_merge($basefields, $fields));
592     } else {
593         throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
594     }
595     if (in_array('*', $fields)) {
596         $fields = array('*');
597     }
599     $orderby = "";
600     $sort    = trim($sort);
601     $sorttimeaccess = false;
602     $allowedsortprefixes = array('c', 'ul', 'ue');
603     if (!empty($sort)) {
604         $rawsorts = explode(',', $sort);
605         $sorts = array();
606         foreach ($rawsorts as $rawsort) {
607             $rawsort = trim($rawsort);
608             if (preg_match('/^ul\.(\S*)\s(asc|desc)/i', $rawsort, $matches)) {
609                 if (strcasecmp($matches[2], 'asc') == 0) {
610                     $sorts[] = 'COALESCE(ul.' . $matches[1] . ', 0) ASC';
611                 } else {
612                     $sorts[] = 'COALESCE(ul.' . $matches[1] . ', 0) DESC';
613                 }
614                 $sorttimeaccess = true;
615             } else if (strpos($rawsort, '.') !== false) {
616                 $prefix = explode('.', $rawsort);
617                 if (in_array($prefix[0], $allowedsortprefixes)) {
618                     $sorts[] = trim($rawsort);
619                 } else {
620                     throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
621                 }
622             } else {
623                 $sorts[] = 'c.'.trim($rawsort);
624             }
625         }
626         $sort = implode(',', $sorts);
627         $orderby = "ORDER BY $sort";
628     }
630     $wheres = array("c.id <> :siteid");
631     $params = array('siteid'=>SITEID);
633     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
634         // list _only_ this course - anything else is asking for trouble...
635         $wheres[] = "courseid = :loginas";
636         $params['loginas'] = $USER->loginascontext->instanceid;
637     }
639     $coursefields = 'c.' .join(',c.', $fields);
640     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
641     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
642     $params['contextlevel'] = CONTEXT_COURSE;
643     $wheres = implode(" AND ", $wheres);
645     $timeaccessselect = "";
646     $timeaccessjoin = "";
648     if (!empty($courseids)) {
649         list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
650         $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
651         $params = array_merge($params, $courseidsparams);
652     }
654     if (!empty($excludecourses)) {
655         list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
656         $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
657         $params = array_merge($params, $courseidsparams);
658     }
660     $courseidsql = "";
661     // Logged-in, non-guest users get their enrolled courses.
662     if (!isguestuser() && isloggedin()) {
663         $courseidsql .= "
664                 SELECT DISTINCT e.courseid
665                   FROM {enrol} e
666                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
667                  WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1
668                        AND (ue.timeend = 0 OR ue.timeend > :now2)";
669         $params['userid1'] = $USER->id;
670         $params['active'] = ENROL_USER_ACTIVE;
671         $params['enabled'] = ENROL_INSTANCE_ENABLED;
672         $params['now1'] = round(time(), -2); // Improves db caching.
673         $params['now2'] = $params['now1'];
675         if ($sorttimeaccess) {
676             $params['userid2'] = $USER->id;
677             $timeaccessselect = ', ul.timeaccess as lastaccessed';
678             $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
679         }
680     }
682     // When including non-enrolled but accessible courses...
683     if ($allaccessible) {
684         if (is_siteadmin()) {
685             // Site admins can access all courses.
686             $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
687         } else {
688             // If we used the enrolment as well, then this will be UNIONed.
689             if ($courseidsql) {
690                 $courseidsql .= " UNION ";
691             }
693             // Include courses with guest access and no password.
694             $courseidsql .= "
695                     SELECT DISTINCT e.courseid
696                       FROM {enrol} e
697                      WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
698             $params['emptypass'] = '';
699             $params['enabled2'] = ENROL_INSTANCE_ENABLED;
701             // Include courses where the current user is currently using guest access (may include
702             // those which require a password).
703             $courseids = [];
704             $accessdata = get_user_accessdata($USER->id);
705             foreach ($accessdata['ra'] as $contextpath => $roles) {
706                 if (array_key_exists($CFG->guestroleid, $roles)) {
707                     // Work out the course id from context path.
708                     $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
709                     if ($context instanceof context_course) {
710                         $courseids[$context->instanceid] = true;
711                     }
712                 }
713             }
715             // Include courses where the current user has moodle/course:view capability.
716             $courses = get_user_capability_course('moodle/course:view', null, false);
717             if (!$courses) {
718                 $courses = [];
719             }
720             foreach ($courses as $course) {
721                 $courseids[$course->id] = true;
722             }
724             // If there are any in either category, list them individually.
725             if ($courseids) {
726                 list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
727                         array_keys($courseids), SQL_PARAMS_NAMED);
728                 $courseidsql .= "
729                         UNION
730                        SELECT DISTINCT c3.id AS courseid
731                          FROM {course} c3
732                         WHERE c3.id $allowedsql";
733                 $params = array_merge($params, $allowedparams);
734             }
735         }
736     }
738     // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
739     // we have the subselect there.
740     $sql = "SELECT $coursefields $ccselect $timeaccessselect
741               FROM {course} c
742               JOIN ($courseidsql) en ON (en.courseid = c.id)
743            $timeaccessjoin
744            $ccjoin
745              WHERE $wheres
746           $orderby";
748     $courses = $DB->get_records_sql($sql, $params, $offset, $limit);
750     // preload contexts and check visibility
751     foreach ($courses as $id=>$course) {
752         context_helper::preload_from_record($course);
753         if (!$course->visible) {
754             if (!$context = context_course::instance($id, IGNORE_MISSING)) {
755                 unset($courses[$id]);
756                 continue;
757             }
758             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
759                 unset($courses[$id]);
760                 continue;
761             }
762         }
763         $courses[$id] = $course;
764     }
766     //wow! Is that really all? :-D
768     return $courses;
771 /**
772  * Returns course enrolment information icons.
773  *
774  * @param object $course
775  * @param array $instances enrol instances of this course, improves performance
776  * @return array of pix_icon
777  */
778 function enrol_get_course_info_icons($course, array $instances = NULL) {
779     $icons = array();
780     if (is_null($instances)) {
781         $instances = enrol_get_instances($course->id, true);
782     }
783     $plugins = enrol_get_plugins(true);
784     foreach ($plugins as $name => $plugin) {
785         $pis = array();
786         foreach ($instances as $instance) {
787             if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
788                 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
789                 continue;
790             }
791             if ($instance->enrol == $name) {
792                 $pis[$instance->id] = $instance;
793             }
794         }
795         if ($pis) {
796             $icons = array_merge($icons, $plugin->get_info_icons($pis));
797         }
798     }
799     return $icons;
802 /**
803  * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
804  *
805  * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
806  * @return string SQL ORDER arguments.
807  */
808 function enrol_get_courses_sortingsql($sort = null) {
809     global $CFG;
811     // Prepare the visible SQL fragment as empty.
812     $visible = '';
813     // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
814     if ($sort === null || strpos($sort, 'visible') === false) {
815         // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
816         // courses to the end of the course list.
817         if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
818             $visible = 'visible DESC, ';
819         }
820     }
822     // Only create a sortorder SQL fragment if the caller didn't already pass one.
823     if ($sort === null) {
824         // If the admin has configured a course sort order, we will use this.
825         if (!empty($CFG->navsortmycoursessort)) {
826             $sort = $CFG->navsortmycoursessort . ' ASC';
828             // Otherwise we will fall back to the sortorder sorting.
829         } else {
830             $sort = 'sortorder ASC';
831         }
832     }
834     return $visible . $sort;
837 /**
838  * Returns course enrolment detailed information.
839  *
840  * @param object $course
841  * @return array of html fragments - can be used to construct lists
842  */
843 function enrol_get_course_description_texts($course) {
844     $lines = array();
845     $instances = enrol_get_instances($course->id, true);
846     $plugins = enrol_get_plugins(true);
847     foreach ($instances as $instance) {
848         if (!isset($plugins[$instance->enrol])) {
849             //weird
850             continue;
851         }
852         $plugin = $plugins[$instance->enrol];
853         $text = $plugin->get_description_text($instance);
854         if ($text !== NULL) {
855             $lines[] = $text;
856         }
857     }
858     return $lines;
861 /**
862  * Returns list of courses user is enrolled into.
863  *
864  * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
865  *
866  * The $fields param is a list of field names to ADD so name just the fields you really need,
867  * which will be added and uniq'd.
868  *
869  * @param int $userid User whose courses are returned, defaults to the current user.
870  * @param bool $onlyactive Return only active enrolments in courses user may see.
871  * @param string|array $fields Extra fields to be returned (array or comma-separated list).
872  * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
873  * @return array
874  */
875 function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
876     global $DB;
878     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
880     // preload contexts and check visibility
881     if ($onlyactive) {
882         foreach ($courses as $id=>$course) {
883             context_helper::preload_from_record($course);
884             if (!$course->visible) {
885                 if (!$context = context_course::instance($id)) {
886                     unset($courses[$id]);
887                     continue;
888                 }
889                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
890                     unset($courses[$id]);
891                     continue;
892                 }
893             }
894         }
895     }
897     return $courses;
900 /**
901  * Returns list of roles per users into course.
902  *
903  * @param int $courseid Course id.
904  * @return array Array[$userid][$roleid] = role_assignment.
905  */
906 function enrol_get_course_users_roles(int $courseid) : array {
907     global $DB;
909     $context = context_course::instance($courseid);
911     $roles = array();
913     $records = $DB->get_recordset('role_assignments', array('contextid' => $context->id));
914     foreach ($records as $record) {
915         if (isset($roles[$record->userid]) === false) {
916             $roles[$record->userid] = array();
917         }
918         $roles[$record->userid][$record->roleid] = $record;
919     }
920     $records->close();
922     return $roles;
925 /**
926  * Can user access at least one enrolled course?
927  *
928  * Cheat if necessary, but find out as fast as possible!
929  *
930  * @param int|stdClass $user null means use current user
931  * @return bool
932  */
933 function enrol_user_sees_own_courses($user = null) {
934     global $USER;
936     if ($user === null) {
937         $user = $USER;
938     }
939     $userid = is_object($user) ? $user->id : $user;
941     // Guest account does not have any courses
942     if (isguestuser($userid) or empty($userid)) {
943         return false;
944     }
946     // Let's cheat here if this is the current user,
947     // if user accessed any course recently, then most probably
948     // we do not need to query the database at all.
949     if ($USER->id == $userid) {
950         if (!empty($USER->enrol['enrolled'])) {
951             foreach ($USER->enrol['enrolled'] as $until) {
952                 if ($until > time()) {
953                     return true;
954                 }
955             }
956         }
957     }
959     // Now the slow way.
960     $courses = enrol_get_all_users_courses($userid, true);
961     foreach($courses as $course) {
962         if ($course->visible) {
963             return true;
964         }
965         context_helper::preload_from_record($course);
966         $context = context_course::instance($course->id);
967         if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
968             return true;
969         }
970     }
972     return false;
975 /**
976  * Returns list of courses user is enrolled into without performing any capability checks.
977  *
978  * The $fields param is a list of field names to ADD so name just the fields you really need,
979  * which will be added and uniq'd.
980  *
981  * @param int $userid User whose courses are returned, defaults to the current user.
982  * @param bool $onlyactive Return only active enrolments in courses user may see.
983  * @param string|array $fields Extra fields to be returned (array or comma-separated list).
984  * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
985  * @return array
986  */
987 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
988     global $DB;
990     // Re-Arrange the course sorting according to the admin settings.
991     $sort = enrol_get_courses_sortingsql($sort);
993     // Guest account does not have any courses
994     if (isguestuser($userid) or empty($userid)) {
995         return(array());
996     }
998     $basefields = array('id', 'category', 'sortorder',
999             'shortname', 'fullname', 'idnumber',
1000             'startdate', 'visible',
1001             'defaultgroupingid',
1002             'groupmode', 'groupmodeforce');
1004     if (empty($fields)) {
1005         $fields = $basefields;
1006     } else if (is_string($fields)) {
1007         // turn the fields from a string to an array
1008         $fields = explode(',', $fields);
1009         $fields = array_map('trim', $fields);
1010         $fields = array_unique(array_merge($basefields, $fields));
1011     } else if (is_array($fields)) {
1012         $fields = array_unique(array_merge($basefields, $fields));
1013     } else {
1014         throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
1015     }
1016     if (in_array('*', $fields)) {
1017         $fields = array('*');
1018     }
1020     $orderby = "";
1021     $sort    = trim($sort);
1022     if (!empty($sort)) {
1023         $rawsorts = explode(',', $sort);
1024         $sorts = array();
1025         foreach ($rawsorts as $rawsort) {
1026             $rawsort = trim($rawsort);
1027             if (strpos($rawsort, 'c.') === 0) {
1028                 $rawsort = substr($rawsort, 2);
1029             }
1030             $sorts[] = trim($rawsort);
1031         }
1032         $sort = 'c.'.implode(',c.', $sorts);
1033         $orderby = "ORDER BY $sort";
1034     }
1036     $params = array('siteid'=>SITEID);
1038     if ($onlyactive) {
1039         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
1040         $params['now1']    = round(time(), -2); // improves db caching
1041         $params['now2']    = $params['now1'];
1042         $params['active']  = ENROL_USER_ACTIVE;
1043         $params['enabled'] = ENROL_INSTANCE_ENABLED;
1044     } else {
1045         $subwhere = "";
1046     }
1048     $coursefields = 'c.' .join(',c.', $fields);
1049     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1050     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1051     $params['contextlevel'] = CONTEXT_COURSE;
1053     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
1054     $sql = "SELECT $coursefields $ccselect
1055               FROM {course} c
1056               JOIN (SELECT DISTINCT e.courseid
1057                       FROM {enrol} e
1058                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1059                  $subwhere
1060                    ) en ON (en.courseid = c.id)
1061            $ccjoin
1062              WHERE c.id <> :siteid
1063           $orderby";
1064     $params['userid']  = $userid;
1066     $courses = $DB->get_records_sql($sql, $params);
1068     return $courses;
1073 /**
1074  * Called when user is about to be deleted.
1075  * @param object $user
1076  * @return void
1077  */
1078 function enrol_user_delete($user) {
1079     global $DB;
1081     $plugins = enrol_get_plugins(true);
1082     foreach ($plugins as $plugin) {
1083         $plugin->user_delete($user);
1084     }
1086     // force cleanup of all broken enrolments
1087     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1090 /**
1091  * Called when course is about to be deleted.
1092  * @param stdClass $course
1093  * @return void
1094  */
1095 function enrol_course_delete($course) {
1096     global $DB;
1098     $instances = enrol_get_instances($course->id, false);
1099     $plugins = enrol_get_plugins(true);
1100     foreach ($instances as $instance) {
1101         if (isset($plugins[$instance->enrol])) {
1102             $plugins[$instance->enrol]->delete_instance($instance);
1103         }
1104         // low level delete in case plugin did not do it
1105         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1106         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1107         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1108         $DB->delete_records('enrol', array('id'=>$instance->id));
1109     }
1112 /**
1113  * Try to enrol user via default internal auth plugin.
1114  *
1115  * For now this is always using the manual enrol plugin...
1116  *
1117  * @param $courseid
1118  * @param $userid
1119  * @param $roleid
1120  * @param $timestart
1121  * @param $timeend
1122  * @return bool success
1123  */
1124 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1125     global $DB;
1127     //note: this is hardcoded to manual plugin for now
1129     if (!enrol_is_enabled('manual')) {
1130         return false;
1131     }
1133     if (!$enrol = enrol_get_plugin('manual')) {
1134         return false;
1135     }
1136     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1137         return false;
1138     }
1139     $instance = reset($instances);
1141     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1143     return true;
1146 /**
1147  * Is there a chance users might self enrol
1148  * @param int $courseid
1149  * @return bool
1150  */
1151 function enrol_selfenrol_available($courseid) {
1152     $result = false;
1154     $plugins = enrol_get_plugins(true);
1155     $enrolinstances = enrol_get_instances($courseid, true);
1156     foreach($enrolinstances as $instance) {
1157         if (!isset($plugins[$instance->enrol])) {
1158             continue;
1159         }
1160         if ($instance->enrol === 'guest') {
1161             // blacklist known temporary guest plugins
1162             continue;
1163         }
1164         if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
1165             $result = true;
1166             break;
1167         }
1168     }
1170     return $result;
1173 /**
1174  * This function returns the end of current active user enrolment.
1175  *
1176  * It deals correctly with multiple overlapping user enrolments.
1177  *
1178  * @param int $courseid
1179  * @param int $userid
1180  * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1181  */
1182 function enrol_get_enrolment_end($courseid, $userid) {
1183     global $DB;
1185     $sql = "SELECT ue.*
1186               FROM {user_enrolments} ue
1187               JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1188               JOIN {user} u ON u.id = ue.userid
1189              WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1190     $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1192     if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1193         return false;
1194     }
1196     $changes = array();
1198     foreach ($enrolments as $ue) {
1199         $start = (int)$ue->timestart;
1200         $end = (int)$ue->timeend;
1201         if ($end != 0 and $end < $start) {
1202             debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1203             continue;
1204         }
1205         if (isset($changes[$start])) {
1206             $changes[$start] = $changes[$start] + 1;
1207         } else {
1208             $changes[$start] = 1;
1209         }
1210         if ($end === 0) {
1211             // no end
1212         } else if (isset($changes[$end])) {
1213             $changes[$end] = $changes[$end] - 1;
1214         } else {
1215             $changes[$end] = -1;
1216         }
1217     }
1219     // let's sort then enrolment starts&ends and go through them chronologically,
1220     // looking for current status and the next future end of enrolment
1221     ksort($changes);
1223     $now = time();
1224     $current = 0;
1225     $present = null;
1227     foreach ($changes as $time => $change) {
1228         if ($time > $now) {
1229             if ($present === null) {
1230                 // we have just went past current time
1231                 $present = $current;
1232                 if ($present < 1) {
1233                     // no enrolment active
1234                     return false;
1235                 }
1236             }
1237             if ($present !== null) {
1238                 // we are already in the future - look for possible end
1239                 if ($current + $change < 1) {
1240                     return $time;
1241                 }
1242             }
1243         }
1244         $current += $change;
1245     }
1247     if ($current > 0) {
1248         return 0;
1249     } else {
1250         return false;
1251     }
1254 /**
1255  * Is current user accessing course via this enrolment method?
1256  *
1257  * This is intended for operations that are going to affect enrol instances.
1258  *
1259  * @param stdClass $instance enrol instance
1260  * @return bool
1261  */
1262 function enrol_accessing_via_instance(stdClass $instance) {
1263     global $DB, $USER;
1265     if (empty($instance->id)) {
1266         return false;
1267     }
1269     if (is_siteadmin()) {
1270         // Admins may go anywhere.
1271         return false;
1272     }
1274     return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1277 /**
1278  * Returns true if user is enrolled (is participating) in course
1279  * this is intended for students and teachers.
1280  *
1281  * Since 2.2 the result for active enrolments and current user are cached.
1282  *
1283  * @param context $context
1284  * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1285  * @param string $withcapability extra capability name
1286  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1287  * @return bool
1288  */
1289 function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1290     global $USER, $DB;
1292     // First find the course context.
1293     $coursecontext = $context->get_course_context();
1295     // Make sure there is a real user specified.
1296     if ($user === null) {
1297         $userid = isset($USER->id) ? $USER->id : 0;
1298     } else {
1299         $userid = is_object($user) ? $user->id : $user;
1300     }
1302     if (empty($userid)) {
1303         // Not-logged-in!
1304         return false;
1305     } else if (isguestuser($userid)) {
1306         // Guest account can not be enrolled anywhere.
1307         return false;
1308     }
1310     // Note everybody participates on frontpage, so for other contexts...
1311     if ($coursecontext->instanceid != SITEID) {
1312         // Try cached info first - the enrolled flag is set only when active enrolment present.
1313         if ($USER->id == $userid) {
1314             $coursecontext->reload_if_dirty();
1315             if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1316                 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1317                     if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1318                         return false;
1319                     }
1320                     return true;
1321                 }
1322             }
1323         }
1325         if ($onlyactive) {
1326             // Look for active enrolments only.
1327             $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1329             if ($until === false) {
1330                 return false;
1331             }
1333             if ($USER->id == $userid) {
1334                 if ($until == 0) {
1335                     $until = ENROL_MAX_TIMESTAMP;
1336                 }
1337                 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1338                 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1339                     unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1340                     remove_temp_course_roles($coursecontext);
1341                 }
1342             }
1344         } else {
1345             // Any enrolment is good for us here, even outdated, disabled or inactive.
1346             $sql = "SELECT 'x'
1347                       FROM {user_enrolments} ue
1348                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1349                       JOIN {user} u ON u.id = ue.userid
1350                      WHERE ue.userid = :userid AND u.deleted = 0";
1351             $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1352             if (!$DB->record_exists_sql($sql, $params)) {
1353                 return false;
1354             }
1355         }
1356     }
1358     if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1359         return false;
1360     }
1362     return true;
1365 /**
1366  * Returns an array of joins, wheres and params that will limit the group of
1367  * users to only those enrolled and with given capability (if specified).
1368  *
1369  * Note this join will return duplicate rows for users who have been enrolled
1370  * several times (e.g. as manual enrolment, and as self enrolment). You may
1371  * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1372  *
1373  * @param context $context
1374  * @param string $prefix optional, a prefix to the user id column
1375  * @param string|array $capability optional, may include a capability name, or array of names.
1376  *      If an array is provided then this is the equivalent of a logical 'OR',
1377  *      i.e. the user needs to have one of these capabilities.
1378  * @param int $group optional, 0 indicates no current group and USERSWITHOUTGROUP users without any group; otherwise the group id
1379  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1380  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1381  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1382  * @return \core\dml\sql_join Contains joins, wheres, params
1383  */
1384 function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
1385         $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1386     $uid = $prefix . 'u.id';
1387     $joins = array();
1388     $wheres = array();
1390     $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1391     $joins[] = $enrolledjoin->joins;
1392     $wheres[] = $enrolledjoin->wheres;
1393     $params = $enrolledjoin->params;
1395     if (!empty($capability)) {
1396         $capjoin = get_with_capability_join($context, $capability, $uid);
1397         $joins[] = $capjoin->joins;
1398         $wheres[] = $capjoin->wheres;
1399         $params = array_merge($params, $capjoin->params);
1400     }
1402     if ($group) {
1403         $groupjoin = groups_get_members_join($group, $uid, $context);
1404         $joins[] = $groupjoin->joins;
1405         $params = array_merge($params, $groupjoin->params);
1406         if (!empty($groupjoin->wheres)) {
1407             $wheres[] = $groupjoin->wheres;
1408         }
1409     }
1411     $joins = implode("\n", $joins);
1412     $wheres[] = "{$prefix}u.deleted = 0";
1413     $wheres = implode(" AND ", $wheres);
1415     return new \core\dml\sql_join($joins, $wheres, $params);
1418 /**
1419  * Returns array with sql code and parameters returning all ids
1420  * of users enrolled into course.
1421  *
1422  * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1423  *
1424  * @param context $context
1425  * @param string $withcapability
1426  * @param int $groupid 0 means ignore groups, USERSWITHOUTGROUP without any group and any other value limits the result by group id
1427  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1428  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1429  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1430  * @return array list($sql, $params)
1431  */
1432 function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false,
1433                           $enrolid = 0) {
1435     // Use unique prefix just in case somebody makes some SQL magic with the result.
1436     static $i = 0;
1437     $i++;
1438     $prefix = 'eu' . $i . '_';
1440     $capjoin = get_enrolled_with_capabilities_join(
1441             $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended, $enrolid);
1443     $sql = "SELECT DISTINCT {$prefix}u.id
1444               FROM {user} {$prefix}u
1445             $capjoin->joins
1446              WHERE $capjoin->wheres";
1448     return array($sql, $capjoin->params);
1451 /**
1452  * Returns array with sql joins and parameters returning all ids
1453  * of users enrolled into course.
1454  *
1455  * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1456  *
1457  * @throws coding_exception
1458  *
1459  * @param context $context
1460  * @param string $useridcolumn User id column used the calling query, e.g. u.id
1461  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1462  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1463  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1464  * @return \core\dml\sql_join Contains joins, wheres, params
1465  */
1466 function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1467     // Use unique prefix just in case somebody makes some SQL magic with the result.
1468     static $i = 0;
1469     $i++;
1470     $prefix = 'ej' . $i . '_';
1472     // First find the course context.
1473     $coursecontext = $context->get_course_context();
1475     $isfrontpage = ($coursecontext->instanceid == SITEID);
1477     if ($onlyactive && $onlysuspended) {
1478         throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1479     }
1480     if ($isfrontpage && $onlysuspended) {
1481         throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1482     }
1484     $joins  = array();
1485     $wheres = array();
1486     $params = array();
1488     $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1490     // Note all users are "enrolled" on the frontpage, but for others...
1491     if (!$isfrontpage) {
1492         $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1493         $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1495         $enrolconditions = array(
1496             "{$prefix}e.id = {$prefix}ue.enrolid",
1497             "{$prefix}e.courseid = :{$prefix}courseid",
1498         );
1499         if ($enrolid) {
1500             $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1501             $params[$prefix . 'enrolid'] = $enrolid;
1502         }
1503         $enrolconditionssql = implode(" AND ", $enrolconditions);
1504         $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1506         $params[$prefix.'courseid'] = $coursecontext->instanceid;
1508         if (!$onlysuspended) {
1509             $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1510             $joins[] = $ejoin;
1511             if ($onlyactive) {
1512                 $wheres[] = "$where1 AND $where2";
1513             }
1514         } else {
1515             // Suspended only where there is enrolment but ALL are suspended.
1516             // Consider multiple enrols where one is not suspended or plain role_assign.
1517             $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1518             $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1519             $enrolconditions = array(
1520                 "{$prefix}e1.id = {$prefix}ue1.enrolid",
1521                 "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1522             );
1523             if ($enrolid) {
1524                 $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1525                 $params[$prefix . 'e1_enrolid'] = $enrolid;
1526             }
1527             $enrolconditionssql = implode(" AND ", $enrolconditions);
1528             $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1529             $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1530             $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1531         }
1533         if ($onlyactive || $onlysuspended) {
1534             $now = round(time(), -2); // Rounding helps caching in DB.
1535             $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1536                     $prefix . 'active' => ENROL_USER_ACTIVE,
1537                     $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1538         }
1539     }
1541     $joins = implode("\n", $joins);
1542     $wheres = implode(" AND ", $wheres);
1544     return new \core\dml\sql_join($joins, $wheres, $params);
1547 /**
1548  * Returns list of users enrolled into course.
1549  *
1550  * @param context $context
1551  * @param string $withcapability
1552  * @param int $groupid 0 means ignore groups, USERSWITHOUTGROUP without any group and any other value limits the result by group id
1553  * @param string $userfields requested user record fields
1554  * @param string $orderby
1555  * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1556  * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1557  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1558  * @return array of user records
1559  */
1560 function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
1561         $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1562     global $DB;
1564     list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
1565     $sql = "SELECT $userfields
1566               FROM {user} u
1567               JOIN ($esql) je ON je.id = u.id
1568              WHERE u.deleted = 0";
1570     if ($orderby) {
1571         $sql = "$sql ORDER BY $orderby";
1572     } else {
1573         list($sort, $sortparams) = users_order_by_sql('u');
1574         $sql = "$sql ORDER BY $sort";
1575         $params = array_merge($params, $sortparams);
1576     }
1578     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1581 /**
1582  * Counts list of users enrolled into course (as per above function)
1583  *
1584  * @param context $context
1585  * @param string $withcapability
1586  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1587  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1588  * @return array of user records
1589  */
1590 function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
1591     global $DB;
1593     $capjoin = get_enrolled_with_capabilities_join(
1594             $context, '', $withcapability, $groupid, $onlyactive);
1596     $sql = "SELECT COUNT(DISTINCT u.id)
1597               FROM {user} u
1598             $capjoin->joins
1599              WHERE $capjoin->wheres AND u.deleted = 0";
1601     return $DB->count_records_sql($sql, $capjoin->params);
1604 /**
1605  * Send welcome email "from" options.
1606  *
1607  * @return array list of from options
1608  */
1609 function enrol_send_welcome_email_options() {
1610     return [
1611         ENROL_DO_NOT_SEND_EMAIL                 => get_string('no'),
1612         ENROL_SEND_EMAIL_FROM_COURSE_CONTACT    => get_string('sendfromcoursecontact', 'enrol'),
1613         ENROL_SEND_EMAIL_FROM_KEY_HOLDER        => get_string('sendfromkeyholder', 'enrol'),
1614         ENROL_SEND_EMAIL_FROM_NOREPLY           => get_string('sendfromnoreply', 'enrol')
1615     ];
1618 /**
1619  * Serve the user enrolment form as a fragment.
1620  *
1621  * @param array $args List of named arguments for the fragment loader.
1622  * @return string
1623  */
1624 function enrol_output_fragment_user_enrolment_form($args) {
1625     global $CFG, $DB;
1627     $args = (object) $args;
1628     $context = $args->context;
1629     require_capability('moodle/course:enrolreview', $context);
1631     $ueid = $args->ueid;
1632     $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1633     $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1634     $plugin = enrol_get_plugin($instance->enrol);
1635     $customdata = [
1636         'ue' => $userenrolment,
1637         'modal' => true,
1638         'enrolinstancename' => $plugin->get_instance_name($instance)
1639     ];
1641     // Set the data if applicable.
1642     $data = [];
1643     if (isset($args->formdata)) {
1644         $serialiseddata = json_decode($args->formdata);
1645         parse_str($serialiseddata, $data);
1646     }
1648     require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1649     $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1651     if (!empty($data)) {
1652         $mform->set_data($data);
1653         $mform->is_validated();
1654     }
1656     return $mform->render();
1659 /**
1660  * Returns the course where a user enrolment belong to.
1661  *
1662  * @param int $ueid user_enrolments id
1663  * @return stdClass
1664  */
1665 function enrol_get_course_by_user_enrolment_id($ueid) {
1666     global $DB;
1667     $sql = "SELECT c.* FROM {user_enrolments} ue
1668               JOIN {enrol} e ON e.id = ue.enrolid
1669               JOIN {course} c ON c.id = e.courseid
1670              WHERE ue.id = :ueid";
1671     return $DB->get_record_sql($sql, array('ueid' => $ueid));
1674 /**
1675  * Return all users enrolled in a course.
1676  *
1677  * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1678  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1679  * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1680  * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1681  * @return stdClass[]
1682  */
1683 function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
1684     global $DB;
1686     if (!$courseid && !$usersfilter && !$uefilter) {
1687         throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1688     }
1690     $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1691              ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1692              ue.timemodified AS uetimemodified, e.status AS estatus,
1693              u.* FROM {user_enrolments} ue
1694               JOIN {enrol} e ON e.id = ue.enrolid
1695               JOIN {user} u ON ue.userid = u.id
1696              WHERE ";
1697     $params = array();
1699     if ($courseid) {
1700         $conditions[] = "e.courseid = :courseid";
1701         $params['courseid'] = $courseid;
1702     }
1704     if ($onlyactive) {
1705         $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1706             "(ue.timeend = 0 OR ue.timeend > :now2)";
1707         // Improves db caching.
1708         $params['now1']    = round(time(), -2);
1709         $params['now2']    = $params['now1'];
1710         $params['active']  = ENROL_USER_ACTIVE;
1711         $params['enabled'] = ENROL_INSTANCE_ENABLED;
1712     }
1714     if ($usersfilter) {
1715         list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1716         $conditions[] = "ue.userid $usersql";
1717         $params = $params + $userparams;
1718     }
1720     if ($uefilter) {
1721         list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1722         $conditions[] = "ue.id $uesql";
1723         $params = $params + $ueparams;
1724     }
1726     return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1729 /**
1730  * Get the list of options for the enrolment period dropdown
1731  *
1732  * @return array List of options for the enrolment period dropdown
1733  */
1734 function enrol_get_period_list() {
1735     $periodmenu = [];
1736     $periodmenu[''] = get_string('unlimited');
1737     for ($i = 1; $i <= 365; $i++) {
1738         $seconds = $i * DAYSECS;
1739         $periodmenu[$seconds] = get_string('numdays', '', $i);
1740     }
1741     return $periodmenu;
1744 /**
1745  * Calculate duration base on start time and end time
1746  *
1747  * @param int $timestart Time start
1748  * @param int $timeend Time end
1749  * @return float|int Calculated duration
1750  */
1751 function enrol_calculate_duration($timestart, $timeend) {
1752     $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1753     return $duration;
1756 /**
1757  * Enrolment plugins abstract class.
1758  *
1759  * All enrol plugins should be based on this class,
1760  * this is also the main source of documentation.
1761  *
1762  * @copyright  2010 Petr Skoda {@link http://skodak.org}
1763  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1764  */
1765 abstract class enrol_plugin {
1766     protected $config = null;
1768     /**
1769      * Returns name of this enrol plugin
1770      * @return string
1771      */
1772     public function get_name() {
1773         // second word in class is always enrol name, sorry, no fancy plugin names with _
1774         $words = explode('_', get_class($this));
1775         return $words[1];
1776     }
1778     /**
1779      * Returns localised name of enrol instance
1780      *
1781      * @param object $instance (null is accepted too)
1782      * @return string
1783      */
1784     public function get_instance_name($instance) {
1785         if (empty($instance->name)) {
1786             $enrol = $this->get_name();
1787             return get_string('pluginname', 'enrol_'.$enrol);
1788         } else {
1789             $context = context_course::instance($instance->courseid);
1790             return format_string($instance->name, true, array('context'=>$context));
1791         }
1792     }
1794     /**
1795      * Returns optional enrolment information icons.
1796      *
1797      * This is used in course list for quick overview of enrolment options.
1798      *
1799      * We are not using single instance parameter because sometimes
1800      * we might want to prevent icon repetition when multiple instances
1801      * of one type exist. One instance may also produce several icons.
1802      *
1803      * @param array $instances all enrol instances of this type in one course
1804      * @return array of pix_icon
1805      */
1806     public function get_info_icons(array $instances) {
1807         return array();
1808     }
1810     /**
1811      * Returns optional enrolment instance description text.
1812      *
1813      * This is used in detailed course information.
1814      *
1815      *
1816      * @param object $instance
1817      * @return string short html text
1818      */
1819     public function get_description_text($instance) {
1820         return null;
1821     }
1823     /**
1824      * Makes sure config is loaded and cached.
1825      * @return void
1826      */
1827     protected function load_config() {
1828         if (!isset($this->config)) {
1829             $name = $this->get_name();
1830             $this->config = get_config("enrol_$name");
1831         }
1832     }
1834     /**
1835      * Returns plugin config value
1836      * @param  string $name
1837      * @param  string $default value if config does not exist yet
1838      * @return string value or default
1839      */
1840     public function get_config($name, $default = NULL) {
1841         $this->load_config();
1842         return isset($this->config->$name) ? $this->config->$name : $default;
1843     }
1845     /**
1846      * Sets plugin config value
1847      * @param  string $name name of config
1848      * @param  string $value string config value, null means delete
1849      * @return string value
1850      */
1851     public function set_config($name, $value) {
1852         $pluginname = $this->get_name();
1853         $this->load_config();
1854         if ($value === NULL) {
1855             unset($this->config->$name);
1856         } else {
1857             $this->config->$name = $value;
1858         }
1859         set_config($name, $value, "enrol_$pluginname");
1860     }
1862     /**
1863      * Does this plugin assign protected roles are can they be manually removed?
1864      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1865      */
1866     public function roles_protected() {
1867         return true;
1868     }
1870     /**
1871      * Does this plugin allow manual enrolments?
1872      *
1873      * @param stdClass $instance course enrol instance
1874      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1875      *
1876      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1877      */
1878     public function allow_enrol(stdClass $instance) {
1879         return false;
1880     }
1882     /**
1883      * Does this plugin allow manual unenrolment of all users?
1884      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1885      *
1886      * @param stdClass $instance course enrol instance
1887      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1888      */
1889     public function allow_unenrol(stdClass $instance) {
1890         return false;
1891     }
1893     /**
1894      * Does this plugin allow manual unenrolment of a specific user?
1895      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1896      *
1897      * This is useful especially for synchronisation plugins that
1898      * do suspend instead of full unenrolment.
1899      *
1900      * @param stdClass $instance course enrol instance
1901      * @param stdClass $ue record from user_enrolments table, specifies user
1902      *
1903      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1904      */
1905     public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1906         return $this->allow_unenrol($instance);
1907     }
1909     /**
1910      * Does this plugin allow manual changes in user_enrolments table?
1911      *
1912      * All plugins allowing this must implement 'enrol/xxx:manage' capability
1913      *
1914      * @param stdClass $instance course enrol instance
1915      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1916      */
1917     public function allow_manage(stdClass $instance) {
1918         return false;
1919     }
1921     /**
1922      * Does this plugin support some way to user to self enrol?
1923      *
1924      * @param stdClass $instance course enrol instance
1925      *
1926      * @return bool - true means show "Enrol me in this course" link in course UI
1927      */
1928     public function show_enrolme_link(stdClass $instance) {
1929         return false;
1930     }
1932     /**
1933      * Attempt to automatically enrol current user in course without any interaction,
1934      * calling code has to make sure the plugin and instance are active.
1935      *
1936      * This should return either a timestamp in the future or false.
1937      *
1938      * @param stdClass $instance course enrol instance
1939      * @return bool|int false means not enrolled, integer means timeend
1940      */
1941     public function try_autoenrol(stdClass $instance) {
1942         global $USER;
1944         return false;
1945     }
1947     /**
1948      * Attempt to automatically gain temporary guest access to course,
1949      * calling code has to make sure the plugin and instance are active.
1950      *
1951      * This should return either a timestamp in the future or false.
1952      *
1953      * @param stdClass $instance course enrol instance
1954      * @return bool|int false means no guest access, integer means timeend
1955      */
1956     public function try_guestaccess(stdClass $instance) {
1957         global $USER;
1959         return false;
1960     }
1962     /**
1963      * Enrol user into course via enrol instance.
1964      *
1965      * @param stdClass $instance
1966      * @param int $userid
1967      * @param int $roleid optional role id
1968      * @param int $timestart 0 means unknown
1969      * @param int $timeend 0 means forever
1970      * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
1971      * @param bool $recovergrades restore grade history
1972      * @return void
1973      */
1974     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
1975         global $DB, $USER, $CFG; // CFG necessary!!!
1977         if ($instance->courseid == SITEID) {
1978             throw new coding_exception('invalid attempt to enrol into frontpage course!');
1979         }
1981         $name = $this->get_name();
1982         $courseid = $instance->courseid;
1984         if ($instance->enrol !== $name) {
1985             throw new coding_exception('invalid enrol instance!');
1986         }
1987         $context = context_course::instance($instance->courseid, MUST_EXIST);
1988         if (!isset($recovergrades)) {
1989             $recovergrades = $CFG->recovergradesdefault;
1990         }
1992         $inserted = false;
1993         $updated  = false;
1994         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1995             //only update if timestart or timeend or status are different.
1996             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
1997                 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
1998             }
1999         } else {
2000             $ue = new stdClass();
2001             $ue->enrolid      = $instance->id;
2002             $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
2003             $ue->userid       = $userid;
2004             $ue->timestart    = $timestart;
2005             $ue->timeend      = $timeend;
2006             $ue->modifierid   = $USER->id;
2007             $ue->timecreated  = time();
2008             $ue->timemodified = $ue->timecreated;
2009             $ue->id = $DB->insert_record('user_enrolments', $ue);
2011             $inserted = true;
2012         }
2014         if ($inserted) {
2015             // Trigger event.
2016             $event = \core\event\user_enrolment_created::create(
2017                     array(
2018                         'objectid' => $ue->id,
2019                         'courseid' => $courseid,
2020                         'context' => $context,
2021                         'relateduserid' => $ue->userid,
2022                         'other' => array('enrol' => $name)
2023                         )
2024                     );
2025             $event->trigger();
2026             // Check if course contacts cache needs to be cleared.
2027             core_course_category::user_enrolment_changed($courseid, $ue->userid,
2028                     $ue->status, $ue->timestart, $ue->timeend);
2029         }
2031         if ($roleid) {
2032             // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2033             if ($this->roles_protected()) {
2034                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2035             } else {
2036                 role_assign($roleid, $userid, $context->id);
2037             }
2038         }
2040         // Recover old grades if present.
2041         if ($recovergrades) {
2042             require_once("$CFG->libdir/gradelib.php");
2043             grade_recover_history_grades($userid, $courseid);
2044         }
2046         // reset current user enrolment caching
2047         if ($userid == $USER->id) {
2048             if (isset($USER->enrol['enrolled'][$courseid])) {
2049                 unset($USER->enrol['enrolled'][$courseid]);
2050             }
2051             if (isset($USER->enrol['tempguest'][$courseid])) {
2052                 unset($USER->enrol['tempguest'][$courseid]);
2053                 remove_temp_course_roles($context);
2054             }
2055         }
2056     }
2058     /**
2059      * Store user_enrolments changes and trigger event.
2060      *
2061      * @param stdClass $instance
2062      * @param int $userid
2063      * @param int $status
2064      * @param int $timestart
2065      * @param int $timeend
2066      * @return void
2067      */
2068     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2069         global $DB, $USER, $CFG;
2071         $name = $this->get_name();
2073         if ($instance->enrol !== $name) {
2074             throw new coding_exception('invalid enrol instance!');
2075         }
2077         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2078             // weird, user not enrolled
2079             return;
2080         }
2082         $modified = false;
2083         if (isset($status) and $ue->status != $status) {
2084             $ue->status = $status;
2085             $modified = true;
2086         }
2087         if (isset($timestart) and $ue->timestart != $timestart) {
2088             $ue->timestart = $timestart;
2089             $modified = true;
2090         }
2091         if (isset($timeend) and $ue->timeend != $timeend) {
2092             $ue->timeend = $timeend;
2093             $modified = true;
2094         }
2096         if (!$modified) {
2097             // no change
2098             return;
2099         }
2101         $ue->modifierid = $USER->id;
2102         $ue->timemodified = time();
2103         $DB->update_record('user_enrolments', $ue);
2105         // User enrolments have changed, so mark user as dirty.
2106         mark_user_dirty($userid);
2108         // Invalidate core_access cache for get_suspended_userids.
2109         cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2111         // Trigger event.
2112         $event = \core\event\user_enrolment_updated::create(
2113                 array(
2114                     'objectid' => $ue->id,
2115                     'courseid' => $instance->courseid,
2116                     'context' => context_course::instance($instance->courseid),
2117                     'relateduserid' => $ue->userid,
2118                     'other' => array('enrol' => $name)
2119                     )
2120                 );
2121         $event->trigger();
2123         core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2124                 $ue->status, $ue->timestart, $ue->timeend);
2125     }
2127     /**
2128      * Unenrol user from course,
2129      * the last unenrolment removes all remaining roles.
2130      *
2131      * @param stdClass $instance
2132      * @param int $userid
2133      * @return void
2134      */
2135     public function unenrol_user(stdClass $instance, $userid) {
2136         global $CFG, $USER, $DB;
2137         require_once("$CFG->dirroot/group/lib.php");
2139         $name = $this->get_name();
2140         $courseid = $instance->courseid;
2142         if ($instance->enrol !== $name) {
2143             throw new coding_exception('invalid enrol instance!');
2144         }
2145         $context = context_course::instance($instance->courseid, MUST_EXIST);
2147         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2148             // weird, user not enrolled
2149             return;
2150         }
2152         // Remove all users groups linked to this enrolment instance.
2153         if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2154             foreach ($gms as $gm) {
2155                 groups_remove_member($gm->groupid, $gm->userid);
2156             }
2157         }
2159         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2160         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2162         // add extra info and trigger event
2163         $ue->courseid  = $courseid;
2164         $ue->enrol     = $name;
2166         $sql = "SELECT 'x'
2167                   FROM {user_enrolments} ue
2168                   JOIN {enrol} e ON (e.id = ue.enrolid)
2169                  WHERE ue.userid = :userid AND e.courseid = :courseid";
2170         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2171             $ue->lastenrol = false;
2173         } else {
2174             // the big cleanup IS necessary!
2175             require_once("$CFG->libdir/gradelib.php");
2177             // remove all remaining roles
2178             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2180             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2181             groups_delete_group_members($courseid, $userid);
2183             grade_user_unenrol($courseid, $userid);
2185             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2187             $ue->lastenrol = true; // means user not enrolled any more
2188         }
2189         // Trigger event.
2190         $event = \core\event\user_enrolment_deleted::create(
2191                 array(
2192                     'courseid' => $courseid,
2193                     'context' => $context,
2194                     'relateduserid' => $ue->userid,
2195                     'objectid' => $ue->id,
2196                     'other' => array(
2197                         'userenrolment' => (array)$ue,
2198                         'enrol' => $name
2199                         )
2200                     )
2201                 );
2202         $event->trigger();
2204         // User enrolments have changed, so mark user as dirty.
2205         mark_user_dirty($userid);
2207         // Check if courrse contacts cache needs to be cleared.
2208         core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2210         // reset current user enrolment caching
2211         if ($userid == $USER->id) {
2212             if (isset($USER->enrol['enrolled'][$courseid])) {
2213                 unset($USER->enrol['enrolled'][$courseid]);
2214             }
2215             if (isset($USER->enrol['tempguest'][$courseid])) {
2216                 unset($USER->enrol['tempguest'][$courseid]);
2217                 remove_temp_course_roles($context);
2218             }
2219         }
2220     }
2222     /**
2223      * Forces synchronisation of user enrolments.
2224      *
2225      * This is important especially for external enrol plugins,
2226      * this function is called for all enabled enrol plugins
2227      * right after every user login.
2228      *
2229      * @param object $user user record
2230      * @return void
2231      */
2232     public function sync_user_enrolments($user) {
2233         // override if necessary
2234     }
2236     /**
2237      * This returns false for backwards compatibility, but it is really recommended.
2238      *
2239      * @since Moodle 3.1
2240      * @return boolean
2241      */
2242     public function use_standard_editing_ui() {
2243         return false;
2244     }
2246     /**
2247      * Return whether or not, given the current state, it is possible to add a new instance
2248      * of this enrolment plugin to the course.
2249      *
2250      * Default implementation is just for backwards compatibility.
2251      *
2252      * @param int $courseid
2253      * @return boolean
2254      */
2255     public function can_add_instance($courseid) {
2256         $link = $this->get_newinstance_link($courseid);
2257         return !empty($link);
2258     }
2260     /**
2261      * Return whether or not, given the current state, it is possible to edit an instance
2262      * of this enrolment plugin in the course. Used by the standard editing UI
2263      * to generate a link to the edit instance form if editing is allowed.
2264      *
2265      * @param stdClass $instance
2266      * @return boolean
2267      */
2268     public function can_edit_instance($instance) {
2269         $context = context_course::instance($instance->courseid);
2271         return has_capability('enrol/' . $instance->enrol . ':config', $context);
2272     }
2274     /**
2275      * Returns link to page which may be used to add new instance of enrolment plugin in course.
2276      * @param int $courseid
2277      * @return moodle_url page url
2278      */
2279     public function get_newinstance_link($courseid) {
2280         // override for most plugins, check if instance already exists in cases only one instance is supported
2281         return NULL;
2282     }
2284     /**
2285      * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2286      */
2287     public function instance_deleteable($instance) {
2288         throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2289                 enrol_plugin::can_delete_instance() instead');
2290     }
2292     /**
2293      * Is it possible to delete enrol instance via standard UI?
2294      *
2295      * @param stdClass  $instance
2296      * @return bool
2297      */
2298     public function can_delete_instance($instance) {
2299         return false;
2300     }
2302     /**
2303      * Is it possible to hide/show enrol instance via standard UI?
2304      *
2305      * @param stdClass $instance
2306      * @return bool
2307      */
2308     public function can_hide_show_instance($instance) {
2309         debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2310         return true;
2311     }
2313     /**
2314      * Returns link to manual enrol UI if exists.
2315      * Does the access control tests automatically.
2316      *
2317      * @param object $instance
2318      * @return moodle_url
2319      */
2320     public function get_manual_enrol_link($instance) {
2321         return NULL;
2322     }
2324     /**
2325      * Returns list of unenrol links for all enrol instances in course.
2326      *
2327      * @param int $instance
2328      * @return moodle_url or NULL if self unenrolment not supported
2329      */
2330     public function get_unenrolself_link($instance) {
2331         global $USER, $CFG, $DB;
2333         $name = $this->get_name();
2334         if ($instance->enrol !== $name) {
2335             throw new coding_exception('invalid enrol instance!');
2336         }
2338         if ($instance->courseid == SITEID) {
2339             return NULL;
2340         }
2342         if (!enrol_is_enabled($name)) {
2343             return NULL;
2344         }
2346         if ($instance->status != ENROL_INSTANCE_ENABLED) {
2347             return NULL;
2348         }
2350         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2351             return NULL;
2352         }
2354         $context = context_course::instance($instance->courseid, MUST_EXIST);
2356         if (!has_capability("enrol/$name:unenrolself", $context)) {
2357             return NULL;
2358         }
2360         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2361             return NULL;
2362         }
2364         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2365     }
2367     /**
2368      * Adds enrol instance UI to course edit form
2369      *
2370      * @param object $instance enrol instance or null if does not exist yet
2371      * @param MoodleQuickForm $mform
2372      * @param object $data
2373      * @param object $context context of existing course or parent category if course does not exist
2374      * @return void
2375      */
2376     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2377         // override - usually at least enable/disable switch, has to add own form header
2378     }
2380     /**
2381      * Adds form elements to add/edit instance form.
2382      *
2383      * @since Moodle 3.1
2384      * @param object $instance enrol instance or null if does not exist yet
2385      * @param MoodleQuickForm $mform
2386      * @param context $context
2387      * @return void
2388      */
2389     public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2390         // Do nothing by default.
2391     }
2393     /**
2394      * Perform custom validation of the data used to edit the instance.
2395      *
2396      * @since Moodle 3.1
2397      * @param array $data array of ("fieldname"=>value) of submitted data
2398      * @param array $files array of uploaded files "element_name"=>tmp_file_path
2399      * @param object $instance The instance data loaded from the DB.
2400      * @param context $context The context of the instance we are editing
2401      * @return array of "element_name"=>"error_description" if there are errors,
2402      *         or an empty array if everything is OK.
2403      */
2404     public function edit_instance_validation($data, $files, $instance, $context) {
2405         // No errors by default.
2406         debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2407         return array();
2408     }
2410     /**
2411      * Validates course edit form data
2412      *
2413      * @param object $instance enrol instance or null if does not exist yet
2414      * @param array $data
2415      * @param object $context context of existing course or parent category if course does not exist
2416      * @return array errors array
2417      */
2418     public function course_edit_validation($instance, array $data, $context) {
2419         return array();
2420     }
2422     /**
2423      * Called after updating/inserting course.
2424      *
2425      * @param bool $inserted true if course just inserted
2426      * @param object $course
2427      * @param object $data form data
2428      * @return void
2429      */
2430     public function course_updated($inserted, $course, $data) {
2431         if ($inserted) {
2432             if ($this->get_config('defaultenrol')) {
2433                 $this->add_default_instance($course);
2434             }
2435         }
2436     }
2438     /**
2439      * Add new instance of enrol plugin.
2440      * @param object $course
2441      * @param array instance fields
2442      * @return int id of new instance, null if can not be created
2443      */
2444     public function add_instance($course, array $fields = NULL) {
2445         global $DB;
2447         if ($course->id == SITEID) {
2448             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2449         }
2451         $instance = new stdClass();
2452         $instance->enrol          = $this->get_name();
2453         $instance->status         = ENROL_INSTANCE_ENABLED;
2454         $instance->courseid       = $course->id;
2455         $instance->enrolstartdate = 0;
2456         $instance->enrolenddate   = 0;
2457         $instance->timemodified   = time();
2458         $instance->timecreated    = $instance->timemodified;
2459         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2461         $fields = (array)$fields;
2462         unset($fields['enrol']);
2463         unset($fields['courseid']);
2464         unset($fields['sortorder']);
2465         foreach($fields as $field=>$value) {
2466             $instance->$field = $value;
2467         }
2469         $instance->id = $DB->insert_record('enrol', $instance);
2471         \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2473         return $instance->id;
2474     }
2476     /**
2477      * Update instance of enrol plugin.
2478      *
2479      * @since Moodle 3.1
2480      * @param stdClass $instance
2481      * @param stdClass $data modified instance fields
2482      * @return boolean
2483      */
2484     public function update_instance($instance, $data) {
2485         global $DB;
2486         $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2487                             'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2488                             'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2489                             'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2490                             'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2491                             'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2493         foreach ($properties as $key) {
2494             if (isset($data->$key)) {
2495                 $instance->$key = $data->$key;
2496             }
2497         }
2498         $instance->timemodified = time();
2500         $update = $DB->update_record('enrol', $instance);
2501         if ($update) {
2502             \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2503         }
2504         return $update;
2505     }
2507     /**
2508      * Add new instance of enrol plugin with default settings,
2509      * called when adding new instance manually or when adding new course.
2510      *
2511      * Not all plugins support this.
2512      *
2513      * @param object $course
2514      * @return int id of new instance or null if no default supported
2515      */
2516     public function add_default_instance($course) {
2517         return null;
2518     }
2520     /**
2521      * Update instance status
2522      *
2523      * Override when plugin needs to do some action when enabled or disabled.
2524      *
2525      * @param stdClass $instance
2526      * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2527      * @return void
2528      */
2529     public function update_status($instance, $newstatus) {
2530         global $DB;
2532         $instance->status = $newstatus;
2533         $DB->update_record('enrol', $instance);
2535         $context = context_course::instance($instance->courseid);
2536         \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2538         // Invalidate all enrol caches.
2539         $context->mark_dirty();
2540     }
2542     /**
2543      * Delete course enrol plugin instance, unenrol all users.
2544      * @param object $instance
2545      * @return void
2546      */
2547     public function delete_instance($instance) {
2548         global $DB;
2550         $name = $this->get_name();
2551         if ($instance->enrol !== $name) {
2552             throw new coding_exception('invalid enrol instance!');
2553         }
2555         //first unenrol all users
2556         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2557         foreach ($participants as $participant) {
2558             $this->unenrol_user($instance, $participant->userid);
2559         }
2560         $participants->close();
2562         // now clean up all remainders that were not removed correctly
2563         if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2564             foreach ($gms as $gm) {
2565                 groups_remove_member($gm->groupid, $gm->userid);
2566             }
2567         }
2568         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2569         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2571         // finally drop the enrol row
2572         $DB->delete_records('enrol', array('id'=>$instance->id));
2574         $context = context_course::instance($instance->courseid);
2575         \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2577         // Invalidate all enrol caches.
2578         $context->mark_dirty();
2579     }
2581     /**
2582      * Creates course enrol form, checks if form submitted
2583      * and enrols user if necessary. It can also redirect.
2584      *
2585      * @param stdClass $instance
2586      * @return string html text, usually a form in a text box
2587      */
2588     public function enrol_page_hook(stdClass $instance) {
2589         return null;
2590     }
2592     /**
2593      * Checks if user can self enrol.
2594      *
2595      * @param stdClass $instance enrolment instance
2596      * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2597      *             used by navigation to improve performance.
2598      * @return bool|string true if successful, else error message or false
2599      */
2600     public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2601         return false;
2602     }
2604     /**
2605      * Return information for enrolment instance containing list of parameters required
2606      * for enrolment, name of enrolment plugin etc.
2607      *
2608      * @param stdClass $instance enrolment instance
2609      * @return array instance info.
2610      */
2611     public function get_enrol_info(stdClass $instance) {
2612         return null;
2613     }
2615     /**
2616      * Adds navigation links into course admin block.
2617      *
2618      * By defaults looks for manage links only.
2619      *
2620      * @param navigation_node $instancesnode
2621      * @param stdClass $instance
2622      * @return void
2623      */
2624     public function add_course_navigation($instancesnode, stdClass $instance) {
2625         if ($this->use_standard_editing_ui()) {
2626             $context = context_course::instance($instance->courseid);
2627             $cap = 'enrol/' . $instance->enrol . ':config';
2628             if (has_capability($cap, $context)) {
2629                 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2630                 $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2631                 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2632             }
2633         }
2634     }
2636     /**
2637      * Returns edit icons for the page with list of instances
2638      * @param stdClass $instance
2639      * @return array
2640      */
2641     public function get_action_icons(stdClass $instance) {
2642         global $OUTPUT;
2644         $icons = array();
2645         if ($this->use_standard_editing_ui()) {
2646             $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2647             $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2648             $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2649                 array('class' => 'iconsmall')));
2650         }
2651         return $icons;
2652     }
2654     /**
2655      * Reads version.php and determines if it is necessary
2656      * to execute the cron job now.
2657      * @return bool
2658      */
2659     public function is_cron_required() {
2660         global $CFG;
2662         $name = $this->get_name();
2663         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2664         $plugin = new stdClass();
2665         include($versionfile);
2666         if (empty($plugin->cron)) {
2667             return false;
2668         }
2669         $lastexecuted = $this->get_config('lastcron', 0);
2670         if ($lastexecuted + $plugin->cron < time()) {
2671             return true;
2672         } else {
2673             return false;
2674         }
2675     }
2677     /**
2678      * Called for all enabled enrol plugins that returned true from is_cron_required().
2679      * @return void
2680      */
2681     public function cron() {
2682     }
2684     /**
2685      * Called when user is about to be deleted
2686      * @param object $user
2687      * @return void
2688      */
2689     public function user_delete($user) {
2690         global $DB;
2692         $sql = "SELECT e.*
2693                   FROM {enrol} e
2694                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2695                  WHERE e.enrol = :name AND ue.userid = :userid";
2696         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2698         $rs = $DB->get_recordset_sql($sql, $params);
2699         foreach($rs as $instance) {
2700             $this->unenrol_user($instance, $user->id);
2701         }
2702         $rs->close();
2703     }
2705     /**
2706      * Returns an enrol_user_button that takes the user to a page where they are able to
2707      * enrol users into the managers course through this plugin.
2708      *
2709      * Optional: If the plugin supports manual enrolments it can choose to override this
2710      * otherwise it shouldn't
2711      *
2712      * @param course_enrolment_manager $manager
2713      * @return enrol_user_button|false
2714      */
2715     public function get_manual_enrol_button(course_enrolment_manager $manager) {
2716         return false;
2717     }
2719     /**
2720      * Gets an array of the user enrolment actions
2721      *
2722      * @param course_enrolment_manager $manager
2723      * @param stdClass $ue
2724      * @return array An array of user_enrolment_actions
2725      */
2726     public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2727         $actions = [];
2728         $context = $manager->get_context();
2729         $instance = $ue->enrolmentinstance;
2730         $params = $manager->get_moodlepage()->url->params();
2731         $params['ue'] = $ue->id;
2733         // Edit enrolment action.
2734         if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2735             $title = get_string('editenrolment', 'enrol');
2736             $icon = new pix_icon('t/edit', $title);
2737             $url = new moodle_url('/enrol/editenrolment.php', $params);
2738             $actionparams = [
2739                 'class' => 'editenrollink',
2740                 'rel' => $ue->id,
2741                 'data-action' => ENROL_ACTION_EDIT
2742             ];
2743             $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2744         }
2746         // Unenrol action.
2747         if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2748             $title = get_string('unenrol', 'enrol');
2749             $icon = new pix_icon('t/delete', $title);
2750             $url = new moodle_url('/enrol/unenroluser.php', $params);
2751             $actionparams = [
2752                 'class' => 'unenrollink',
2753                 'rel' => $ue->id,
2754                 'data-action' => ENROL_ACTION_UNENROL
2755             ];
2756             $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2757         }
2758         return $actions;
2759     }
2761     /**
2762      * Returns true if the plugin has one or more bulk operations that can be performed on
2763      * user enrolments.
2764      *
2765      * @param course_enrolment_manager $manager
2766      * @return bool
2767      */
2768     public function has_bulk_operations(course_enrolment_manager $manager) {
2769        return false;
2770     }
2772     /**
2773      * Return an array of enrol_bulk_enrolment_operation objects that define
2774      * the bulk actions that can be performed on user enrolments by the plugin.
2775      *
2776      * @param course_enrolment_manager $manager
2777      * @return array
2778      */
2779     public function get_bulk_operations(course_enrolment_manager $manager) {
2780         return array();
2781     }
2783     /**
2784      * Do any enrolments need expiration processing.
2785      *
2786      * Plugins that want to call this functionality must implement 'expiredaction' config setting.
2787      *
2788      * @param progress_trace $trace
2789      * @param int $courseid one course, empty mean all
2790      * @return bool true if any data processed, false if not
2791      */
2792     public function process_expirations(progress_trace $trace, $courseid = null) {
2793         global $DB;
2795         $name = $this->get_name();
2796         if (!enrol_is_enabled($name)) {
2797             $trace->finished();
2798             return false;
2799         }
2801         $processed = false;
2802         $params = array();
2803         $coursesql = "";
2804         if ($courseid) {
2805             $coursesql = "AND e.courseid = :courseid";
2806         }
2808         // Deal with expired accounts.
2809         $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
2811         if ($action == ENROL_EXT_REMOVED_UNENROL) {
2812             $instances = array();
2813             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2814                       FROM {user_enrolments} ue
2815                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2816                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2817                      WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
2818             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
2820             $rs = $DB->get_recordset_sql($sql, $params);
2821             foreach ($rs as $ue) {
2822                 if (!$processed) {
2823                     $trace->output("Starting processing of enrol_$name expirations...");
2824                     $processed = true;
2825                 }
2826                 if (empty($instances[$ue->enrolid])) {
2827                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2828                 }
2829                 $instance = $instances[$ue->enrolid];
2830                 if (!$this->roles_protected()) {
2831                     // Let's just guess what extra roles are supposed to be removed.
2832                     if ($instance->roleid) {
2833                         role_unassign($instance->roleid, $ue->userid, $ue->contextid);
2834                     }
2835                 }
2836                 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
2837                 $this->unenrol_user($instance, $ue->userid);
2838                 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
2839             }
2840             $rs->close();
2841             unset($instances);
2843         } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
2844             $instances = array();
2845             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2846                       FROM {user_enrolments} ue
2847                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2848                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2849                      WHERE ue.timeend > 0 AND ue.timeend < :now
2850                            AND ue.status = :useractive $coursesql";
2851             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
2852             $rs = $DB->get_recordset_sql($sql, $params);
2853             foreach ($rs as $ue) {
2854                 if (!$processed) {
2855                     $trace->output("Starting processing of enrol_$name expirations...");
2856                     $processed = true;
2857                 }
2858                 if (empty($instances[$ue->enrolid])) {
2859                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2860                 }
2861                 $instance = $instances[$ue->enrolid];
2863                 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
2864                     if (!$this->roles_protected()) {
2865                         // Let's just guess what roles should be removed.
2866                         $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
2867                         if ($count == 1) {
2868                             role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
2870                         } else if ($count > 1 and $instance->roleid) {
2871                             role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
2872                         }
2873                     }
2874                     // In any case remove all roles that belong to this instance and user.
2875                     role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
2876                     // Final cleanup of subcontexts if there are no more course roles.
2877                     if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
2878                         role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
2879                     }
2880                 }
2882                 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
2883                 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
2884             }
2885             $rs->close();
2886             unset($instances);
2888         } else {
2889             // ENROL_EXT_REMOVED_KEEP means no changes.
2890         }
2892         if ($processed) {
2893             $trace->output("...finished processing of enrol_$name expirations");
2894         } else {
2895             $trace->output("No expired enrol_$name enrolments detected");
2896         }
2897         $trace->finished();
2899         return $processed;
2900     }
2902     /**
2903      * Send expiry notifications.
2904      *
2905      * Plugin that wants to have expiry notification MUST implement following:
2906      * - expirynotifyhour plugin setting,
2907      * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
2908      * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
2909      *   expirymessageenrolledsubject and expirymessageenrolledbody),
2910      * - expiry_notification provider in db/messages.php,
2911      * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2912      * - something that calls this method, such as cron.
2913      *
2914      * @param progress_trace $trace (accepts bool for backwards compatibility only)
2915      */
2916     public function send_expiry_notifications($trace) {
2917         global $DB, $CFG;
2919         $name = $this->get_name();
2920         if (!enrol_is_enabled($name)) {
2921             $trace->finished();
2922             return;
2923         }
2925         // Unfortunately this may take a long time, it should not be interrupted,
2926         // otherwise users get duplicate notification.
2928         core_php_time_limit::raise();
2929         raise_memory_limit(MEMORY_HUGE);
2932         $expirynotifylast = $this->get_config('expirynotifylast', 0);
2933         $expirynotifyhour = $this->get_config('expirynotifyhour');
2934         if (is_null($expirynotifyhour)) {
2935             debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
2936             $trace->finished();
2937             return;
2938         }
2940         if (!($trace instanceof progress_trace)) {
2941             $trace = $trace ? new text_progress_trace() : new null_progress_trace();
2942             debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
2943         }
2945         $timenow = time();
2946         $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
2948         if ($expirynotifylast > $notifytime) {
2949             $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
2950             $trace->finished();
2951             return;
2953         } else if ($timenow < $notifytime) {
2954             $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
2955             $trace->finished();
2956             return;
2957         }
2959         $trace->output('Processing '.$name.' enrolment expiration notifications...');
2961         // Notify users responsible for enrolment once every day.
2962         $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
2963                   FROM {user_enrolments} ue
2964                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
2965                   JOIN {course} c ON (c.id = e.courseid)
2966                   JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
2967                  WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
2968               ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
2969         $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
2971         $rs = $DB->get_recordset_sql($sql, $params);
2973         $lastenrollid = 0;
2974         $users = array();
2976         foreach($rs as $ue) {
2977             if ($lastenrollid and $lastenrollid != $ue->enrolid) {
2978                 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2979                 $users = array();
2980             }
2981             $lastenrollid = $ue->enrolid;
2983             $enroller = $this->get_enroller($ue->enrolid);
2984             $context = context_course::instance($ue->courseid);
2986             $user = $DB->get_record('user', array('id'=>$ue->userid));
2988             $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
2990             if (!$ue->notifyall) {
2991                 continue;
2992             }
2994             if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
2995                 // Notify enrolled users only once at the start of the threshold.
2996                 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2997                 continue;
2998             }
3000             $this->notify_expiry_enrolled($user, $ue, $trace);
3001         }
3002         $rs->close();
3004         if ($lastenrollid and $users) {
3005             $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3006         }
3008         $trace->output('...notification processing finished.');
3009         $trace->finished();
3011         $this->set_config('expirynotifylast', $timenow);
3012     }
3014     /**
3015      * Returns the user who is responsible for enrolments for given instance.
3016      *
3017      * Override if plugin knows anybody better than admin.
3018      *
3019      * @param int $instanceid enrolment instance id
3020      * @return stdClass user record
3021      */
3022     protected function get_enroller($instanceid) {
3023         return get_admin();
3024     }
3026     /**
3027      * Notify user about incoming expiration of their enrolment,
3028      * it is called only if notification of enrolled users (aka students) is enabled in course.
3029      *
3030      * This is executed only once for each expiring enrolment right
3031      * at the start of the expiration threshold.
3032      *
3033      * @param stdClass $user
3034      * @param stdClass $ue
3035      * @param progress_trace $trace
3036      */
3037     protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3038         global $CFG;
3040         $name = $this->get_name();
3042         $oldforcelang = force_current_language($user->lang);
3044         $enroller = $this->get_enroller($ue->enrolid);
3045         $context = context_course::instance($ue->courseid);
3047         $a = new stdClass();
3048         $a->course   = format_string($ue->fullname, true, array('context'=>$context));
3049         $a->user     = fullname($user, true);
3050         $a->timeend  = userdate($ue->timeend, '', $user->timezone);
3051         $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3053         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
3054         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3056         $message = new \core\message\message();
3057         $message->courseid          = $ue->courseid;
3058         $message->notification      = 1;
3059         $message->component         = 'enrol_'.$name;
3060         $message->name              = 'expiry_notification';
3061         $message->userfrom          = $enroller;
3062         $message->userto            = $user;
3063         $message->subject           = $subject;
3064         $message->fullmessage       = $body;
3065         $message->fullmessageformat = FORMAT_MARKDOWN;
3066         $message->fullmessagehtml   = markdown_to_html($body);
3067         $message->smallmessage      = $subject;
3068         $message->contexturlname    = $a->course;
3069         $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
3071         if (message_send($message)) {
3072             $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3073         } else {
3074             $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3075         }
3077         force_current_language($oldforcelang);
3078     }
3080     /**
3081      * Notify person responsible for enrolments that some user enrolments will be expired soon,
3082      * it is called only if notification of enrollers (aka teachers) is enabled in course.
3083      *
3084      * This is called repeatedly every day for each course if there are any pending expiration
3085      * in the expiration threshold.
3086      *
3087      * @param int $eid
3088      * @param array $users
3089      * @param progress_trace $trace
3090      */
3091     protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3092         global $DB;
3094         $name = $this->get_name();
3096         $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3097         $context = context_course::instance($instance->courseid);
3098         $course = $DB->get_record('course', array('id'=>$instance->courseid));
3100         $enroller = $this->get_enroller($instance->id);
3101         $admin = get_admin();
3103         $oldforcelang = force_current_language($enroller->lang);
3105         foreach($users as $key=>$info) {
3106             $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3107         }
3109         $a = new stdClass();
3110         $a->course    = format_string($course->fullname, true, array('context'=>$context));
3111         $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3112         $a->users     = implode("\n", $users);
3113         $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3115         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3116         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3118         $message = new \core\message\message();
3119         $message->courseid          = $course->id;
3120         $message->notification      = 1;
3121         $message->component         = 'enrol_'.$name;
3122         $message->name              = 'expiry_notification';
3123         $message->userfrom          = $admin;
3124         $message->userto            = $enroller;
3125         $message->subject           = $subject;
3126         $message->fullmessage       = $body;
3127         $message->fullmessageformat = FORMAT_MARKDOWN;
3128         $message->fullmessagehtml   = markdown_to_html($body);
3129         $message->smallmessage      = $subject;
3130         $message->contexturlname    = $a->course;
3131         $message->contexturl        = $a->extendurl;
3133         if (message_send($message)) {
3134             $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3135         } else {
3136             $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3137         }
3139         force_current_language($oldforcelang);
3140     }
3142     /**
3143      * Backup execution step hook to annotate custom fields.
3144      *
3145      * @param backup_enrolments_execution_step $step
3146      * @param stdClass $enrol
3147      */
3148     public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3149         // Override as necessary to annotate custom fields in the enrol table.
3150     }
3152     /**
3153      * Automatic enrol sync executed during restore.
3154      * Useful for automatic sync by course->idnumber or course category.
3155      * @param stdClass $course course record
3156      */
3157     public function restore_sync_course($course) {
3158         // Override if necessary.
3159     }
3161     /**
3162      * Restore instance and map settings.
3163      *
3164      * @param restore_enrolments_structure_step $step
3165      * @param stdClass $data
3166      * @param stdClass $course
3167      * @param int $oldid
3168      */
3169     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3170         // Do not call this from overridden methods, restore and set new id there.
3171         $step->set_mapping('enrol', $oldid, 0);
3172     }
3174     /**
3175      * Restore user enrolment.
3176      *
3177      * @param restore_enrolments_structure_step $step
3178      * @param stdClass $data
3179      * @param stdClass $instance
3180      * @param int $oldinstancestatus
3181      * @param int $userid
3182      */
3183     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3184         // Override as necessary if plugin supports restore of enrolments.
3185     }
3187     /**
3188      * Restore role assignment.
3189      *
3190      * @param stdClass $instance
3191      * @param int $roleid
3192      * @param int $userid
3193      * @param int $contextid
3194      */
3195     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3196         // No role assignment by default, override if necessary.
3197     }
3199     /**
3200      * Restore user group membership.
3201      * @param stdClass $instance
3202      * @param int $groupid
3203      * @param int $userid
3204      */
3205     public function restore_group_member($instance, $groupid, $userid) {
3206         // Implement if you want to restore protected group memberships,
3207         // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3208     }
3210     /**
3211      * Returns defaults for new instances.
3212      * @since Moodle 3.1
3213      * @return array
3214      */
3215     public function get_instance_defaults() {
3216         return array();
3217     }
3219     /**
3220      * Validate a list of parameter names and types.
3221      * @since Moodle 3.1
3222      *
3223      * @param array $data array of ("fieldname"=>value) of submitted data
3224      * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3225      * @return array of "element_name"=>"error_description" if there are errors,
3226      *         or an empty array if everything is OK.
3227      */
3228     public function validate_param_types($data, $rules) {
3229         $errors = array();
3230         $invalidstr = get_string('invaliddata', 'error');
3231         foreach ($rules as $fieldname => $rule) {
3232             if (is_array($rule)) {
3233                 if (!in_array($data[$fieldname], $rule)) {
3234                     $errors[$fieldname] = $invalidstr;
3235                 }
3236             } else {
3237                 if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3238                     $errors[$fieldname] = $invalidstr;
3239                 }
3240             }
3241         }
3242         return $errors;
3243     }