MDL-76843 quiz: add test to verify random essay stats now work
[moodle.git] / lib / enrollib.php
blob186ade3abdacfd1b4fe7290f6a2435ad793723ee
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.
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.
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
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.
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.
73 define('ENROL_DO_NOT_SEND_EMAIL', 0);
75 /**
76 * Send email from course contact.
78 define('ENROL_SEND_EMAIL_FROM_COURSE_CONTACT', 1);
80 /**
81 * Send email from enrolment key holder.
83 define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2);
85 /**
86 * Send email from no reply address.
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
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";
113 } else {
114 // sorted alphabetically
115 $plugins = core_component::get_plugin_list('enrol');
116 ksort($plugins);
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;
125 include_once("$location/lib.php");
126 if (!class_exists($class)) {
127 continue;
131 $result[$plugin] = new $class();
134 return $result;
138 * Returns instance of enrol plugin
139 * @param string $name name of enrol plugin ('manual', 'guest', ...)
140 * @return enrol_plugin
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;
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;
159 include_once("$location/lib.php");
160 if (!class_exists($class)) {
161 return null;
165 return new $class();
169 * Returns enrolment instances in given course.
170 * @param int $courseid
171 * @param bool $enabled
172 * @return array of enrol instances
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');
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;
189 if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
190 // broken plugin
191 unset($result[$key]);
192 continue;
196 return $result;
200 * Checks if a given plugin is in the list of enabled enrolment plugins.
202 * @param string $enrol Enrolment plugin name
203 * @return boolean Whether the plugin is enabled
205 function enrol_is_enabled($enrol) {
206 global $CFG;
208 if (empty($CFG->enrol_plugins_enabled)) {
209 return false;
211 return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
215 * Check all the login enrolment information for the given user object
216 * by querying the enrolment plugins
217 * This function may be very slow, use only once after log-in or login-as.
219 * @param stdClass $user User object.
220 * @param bool $ignoreintervalcheck Force to ignore checking configured sync intervals.
222 * @return void
224 function enrol_check_plugins($user, bool $ignoreintervalcheck = true) {
225 global $CFG;
227 if (empty($user->id) or isguestuser($user)) {
228 // shortcut - there is no enrolment work for guests and not-logged-in users
229 return;
232 // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
233 // which proved it was actually not necessary.
235 static $inprogress = array(); // To prevent this function being called more than once in an invocation
237 if (!empty($inprogress[$user->id])) {
238 return;
241 $syncinterval = isset($CFG->enrolments_sync_interval) ? (int)$CFG->enrolments_sync_interval : HOURSECS;
242 $needintervalchecking = !$ignoreintervalcheck && !empty($syncinterval);
244 if ($needintervalchecking) {
245 $lastsync = get_user_preferences('last_time_enrolments_synced', 0, $user);
246 if (time() - $lastsync < $syncinterval) {
247 return;
251 $inprogress[$user->id] = true; // Set the flag
253 $enabled = enrol_get_plugins(true);
255 foreach($enabled as $enrol) {
256 $enrol->sync_user_enrolments($user);
259 if ($needintervalchecking) {
260 set_user_preference('last_time_enrolments_synced', time(), $user);
263 unset($inprogress[$user->id]); // Unset the flag
267 * Do these two students share any course?
269 * The courses has to be visible and enrolments has to be active,
270 * timestart and timeend restrictions are ignored.
272 * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
273 * to true.
275 * @param stdClass|int $user1
276 * @param stdClass|int $user2
277 * @return bool
279 function enrol_sharing_course($user1, $user2) {
280 return enrol_get_shared_courses($user1, $user2, false, true);
284 * Returns any courses shared by the two users
286 * The courses has to be visible and enrolments has to be active,
287 * timestart and timeend restrictions are ignored.
289 * @global moodle_database $DB
290 * @param stdClass|int $user1
291 * @param stdClass|int $user2
292 * @param bool $preloadcontexts If set to true contexts for the returned courses
293 * will be preloaded.
294 * @param bool $checkexistsonly If set to true then this function will return true
295 * if the users share any courses and false if not.
296 * @return array|bool An array of courses that both users are enrolled in OR if
297 * $checkexistsonly set returns true if the users share any courses
298 * and false if not.
300 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
301 global $DB, $CFG;
303 $user1 = isset($user1->id) ? $user1->id : $user1;
304 $user2 = isset($user2->id) ? $user2->id : $user2;
306 if (empty($user1) or empty($user2)) {
307 return false;
310 if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
311 return false;
314 list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
315 list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
316 $params = array_merge($params1, $params2);
317 $params['enabled1'] = ENROL_INSTANCE_ENABLED;
318 $params['enabled2'] = ENROL_INSTANCE_ENABLED;
319 $params['active1'] = ENROL_USER_ACTIVE;
320 $params['active2'] = ENROL_USER_ACTIVE;
321 $params['user1'] = $user1;
322 $params['user2'] = $user2;
324 $ctxselect = '';
325 $ctxjoin = '';
326 if ($preloadcontexts) {
327 $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
328 $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
329 $params['contextlevel'] = CONTEXT_COURSE;
332 $sql = "SELECT c.* $ctxselect
333 FROM {course} c
334 JOIN (
335 SELECT DISTINCT c.id
336 FROM {course} c
337 JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
338 JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
339 JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
340 JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
341 WHERE c.visible = 1
342 ) ec ON ec.id = c.id
343 $ctxjoin";
345 if ($checkexistsonly) {
346 return $DB->record_exists_sql($sql, $params);
347 } else {
348 $courses = $DB->get_records_sql($sql, $params);
349 if ($preloadcontexts) {
350 array_map('context_helper::preload_from_record', $courses);
352 return $courses;
357 * This function adds necessary enrol plugins UI into the course edit form.
359 * @param MoodleQuickForm $mform
360 * @param object $data course edit form data
361 * @param object $context context of existing course or parent category if course does not exist
362 * @return void
364 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
365 $plugins = enrol_get_plugins(true);
366 if (!empty($data->id)) {
367 $instances = enrol_get_instances($data->id, false);
368 foreach ($instances as $instance) {
369 if (!isset($plugins[$instance->enrol])) {
370 continue;
372 $plugin = $plugins[$instance->enrol];
373 $plugin->course_edit_form($instance, $mform, $data, $context);
375 } else {
376 foreach ($plugins as $plugin) {
377 $plugin->course_edit_form(NULL, $mform, $data, $context);
383 * Validate course edit form data
385 * @param array $data raw form data
386 * @param object $context context of existing course or parent category if course does not exist
387 * @return array errors array
389 function enrol_course_edit_validation(array $data, $context) {
390 $errors = array();
391 $plugins = enrol_get_plugins(true);
393 if (!empty($data['id'])) {
394 $instances = enrol_get_instances($data['id'], false);
395 foreach ($instances as $instance) {
396 if (!isset($plugins[$instance->enrol])) {
397 continue;
399 $plugin = $plugins[$instance->enrol];
400 $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
402 } else {
403 foreach ($plugins as $plugin) {
404 $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
408 return $errors;
412 * Update enrol instances after course edit form submission
413 * @param bool $inserted true means new course added, false course already existed
414 * @param object $course
415 * @param object $data form data
416 * @return void
418 function enrol_course_updated($inserted, $course, $data) {
419 global $DB, $CFG;
421 $plugins = enrol_get_plugins(true);
423 foreach ($plugins as $plugin) {
424 $plugin->course_updated($inserted, $course, $data);
429 * Add navigation nodes
430 * @param navigation_node $coursenode
431 * @param object $course
432 * @return void
434 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
435 global $CFG;
437 $coursecontext = context_course::instance($course->id);
439 $instances = enrol_get_instances($course->id, true);
440 $plugins = enrol_get_plugins(true);
442 // we do not want to break all course pages if there is some borked enrol plugin, right?
443 foreach ($instances as $k=>$instance) {
444 if (!isset($plugins[$instance->enrol])) {
445 unset($instances[$k]);
449 $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
451 // List all participants - allows assigning roles, groups, etc.
452 // Have this available even in the site context as the page is still accessible from the frontpage.
453 if (has_capability('moodle/course:enrolreview', $coursecontext)) {
454 $url = new moodle_url('/user/index.php', array('id' => $course->id));
455 $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING,
456 null, 'review', new pix_icon('i/enrolusers', ''));
459 if ($course->id != SITEID) {
460 // manage enrol plugin instances
461 if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
462 $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
463 } else {
464 $url = NULL;
466 $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
468 // each instance decides how to configure itself or how many other nav items are exposed
469 foreach ($instances as $instance) {
470 if (!isset($plugins[$instance->enrol])) {
471 continue;
473 $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
476 if (!$url) {
477 $instancesnode->trim_if_empty();
481 // Manage groups in this course or even frontpage
482 if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
483 $url = new moodle_url('/group/index.php', array('id'=>$course->id));
484 $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
487 if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
488 // Override roles
489 if (has_capability('moodle/role:review', $coursecontext)) {
490 $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
491 } else {
492 $url = NULL;
494 $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
496 // Add assign or override roles if allowed
497 if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
498 if (has_capability('moodle/role:assign', $coursecontext)) {
499 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
500 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
503 // Check role permissions
504 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
505 $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
506 $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
510 // Deal somehow with users that are not enrolled but still got a role somehow
511 if ($course->id != SITEID) {
512 //TODO, create some new UI for role assignments at course level
513 if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
514 $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
515 $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
519 // just in case nothing was actually added
520 $usersnode->trim_if_empty();
522 if ($course->id != SITEID) {
523 if (isguestuser() or !isloggedin()) {
524 // guest account can not be enrolled - no links for them
525 } else if (is_enrolled($coursecontext)) {
526 // unenrol link if possible
527 foreach ($instances as $instance) {
528 if (!isset($plugins[$instance->enrol])) {
529 continue;
531 $plugin = $plugins[$instance->enrol];
532 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
533 $coursenode->add(get_string('unenrolme', 'core_enrol'), $unenrollink,
534 navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
535 $coursenode->get('unenrolself')->set_force_into_more_menu(true);
536 break;
537 //TODO. deal with multiple unenrol links - not likely case, but still...
540 } else {
541 // enrol link if possible
542 if (is_viewing($coursecontext)) {
543 // better not show any enrol link, this is intended for managers and inspectors
544 } else {
545 foreach ($instances as $instance) {
546 if (!isset($plugins[$instance->enrol])) {
547 continue;
549 $plugin = $plugins[$instance->enrol];
550 if ($plugin->show_enrolme_link($instance)) {
551 $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
552 $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
553 $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
554 break;
563 * Returns list of courses current $USER is enrolled in and can access
565 * The $fields param is a list of field names to ADD so name just the fields you really need,
566 * which will be added and uniq'd.
568 * If $allaccessible is true, this will additionally return courses that the current user is not
569 * enrolled in, but can access because they are open to the user for other reasons (course view
570 * permission, currently viewing course as a guest, or course allows guest access without
571 * password).
573 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
574 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
575 * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
576 * "ue" for the user_enrolments table.
577 * @param int $limit max number of courses
578 * @param array $courseids the list of course ids to filter by
579 * @param bool $allaccessible Include courses user is not enrolled in, but can access
580 * @param int $offset Offset the result set by this number
581 * @param array $excludecourses IDs of hidden courses to exclude from search
582 * @return array
584 function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
585 $offset = 0, $excludecourses = []) {
586 global $DB, $USER, $CFG;
588 // Allowed prefixes and field names.
589 $allowedprefixesandfields = ['c' => array_keys($DB->get_columns('course')),
590 'ul' => array_keys($DB->get_columns('user_lastaccess')),
591 'ue' => array_keys($DB->get_columns('user_enrolments'))];
593 // Re-Arrange the course sorting according to the admin settings.
594 $sort = enrol_get_courses_sortingsql($sort);
596 // Guest account does not have any enrolled courses.
597 if (!$allaccessible && (isguestuser() or !isloggedin())) {
598 return array();
601 $basefields = [
602 'id', 'category', 'sortorder',
603 'shortname', 'fullname', 'idnumber',
604 'startdate', 'visible',
605 'groupmode', 'groupmodeforce', 'cacherev',
606 'showactivitydates', 'showcompletionconditions',
609 if (empty($fields)) {
610 $fields = $basefields;
611 } else if (is_string($fields)) {
612 // turn the fields from a string to an array
613 $fields = explode(',', $fields);
614 $fields = array_map('trim', $fields);
615 $fields = array_unique(array_merge($basefields, $fields));
616 } else if (is_array($fields)) {
617 $fields = array_unique(array_merge($basefields, $fields));
618 } else {
619 throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
621 if (in_array('*', $fields)) {
622 $fields = array('*');
625 $orderby = "";
626 $sort = trim($sort);
627 $sorttimeaccess = false;
628 if (!empty($sort)) {
629 $rawsorts = explode(',', $sort);
630 $sorts = array();
631 foreach ($rawsorts as $rawsort) {
632 $rawsort = trim($rawsort);
633 // Make sure that there are no more white spaces in sortparams after explode.
634 $sortparams = array_values(array_filter(explode(' ', $rawsort)));
635 // If more than 2 values present then throw coding_exception.
636 if (isset($sortparams[2])) {
637 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
639 // Check the sort ordering if present, at the beginning.
640 if (isset($sortparams[1]) && (preg_match("/^(asc|desc)$/i", $sortparams[1]) === 0)) {
641 throw new coding_exception('Invalid sort direction in $sort parameter in enrol_get_my_courses()');
644 $sortfield = $sortparams[0];
645 $sortdirection = $sortparams[1] ?? 'asc';
646 if (strpos($sortfield, '.') !== false) {
647 $sortfieldparams = explode('.', $sortfield);
648 // Check if more than one dots present in the prefix field.
649 if (isset($sortfieldparams[2])) {
650 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
652 list($prefix, $fieldname) = [$sortfieldparams[0], $sortfieldparams[1]];
653 // Check if the field name matches with the allowed prefix.
654 if (array_key_exists($prefix, $allowedprefixesandfields) &&
655 (in_array($fieldname, $allowedprefixesandfields[$prefix]))) {
656 if ($prefix === 'ul') {
657 $sorts[] = "COALESCE({$prefix}.{$fieldname}, 0) {$sortdirection}";
658 $sorttimeaccess = true;
659 } else {
660 // Check if the field name that matches with the prefix and just append to sorts.
661 $sorts[] = $rawsort;
663 } else {
664 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
666 } else {
667 // Check if the field name matches with $allowedprefixesandfields.
668 $found = false;
669 foreach (array_keys($allowedprefixesandfields) as $prefix) {
670 if (in_array($sortfield, $allowedprefixesandfields[$prefix])) {
671 if ($prefix === 'ul') {
672 $sorts[] = "COALESCE({$prefix}.{$sortfield}, 0) {$sortdirection}";
673 $sorttimeaccess = true;
674 } else {
675 $sorts[] = "{$prefix}.{$sortfield} {$sortdirection}";
677 $found = true;
678 break;
681 if (!$found) {
682 // The param is not found in $allowedprefixesandfields.
683 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
687 $sort = implode(',', $sorts);
688 $orderby = "ORDER BY $sort";
691 $wheres = ['c.id <> ' . SITEID];
692 $params = [];
694 if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
695 // list _only_ this course - anything else is asking for trouble...
696 $wheres[] = "courseid = :loginas";
697 $params['loginas'] = $USER->loginascontext->instanceid;
700 $coursefields = 'c.' .join(',c.', $fields);
701 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
702 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
703 $params['contextlevel'] = CONTEXT_COURSE;
704 $wheres = implode(" AND ", $wheres);
706 $timeaccessselect = "";
707 $timeaccessjoin = "";
709 if (!empty($courseids)) {
710 list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
711 $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
712 $params = array_merge($params, $courseidsparams);
715 if (!empty($excludecourses)) {
716 list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
717 $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
718 $params = array_merge($params, $courseidsparams);
721 $courseidsql = "";
722 // Logged-in, non-guest users get their enrolled courses.
723 if (!isguestuser() && isloggedin()) {
724 $courseidsql .= "
725 SELECT DISTINCT e.courseid
726 FROM {enrol} e
727 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
728 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart <= :now1
729 AND (ue.timeend = 0 OR ue.timeend > :now2)";
730 $params['userid1'] = $USER->id;
731 $params['active'] = ENROL_USER_ACTIVE;
732 $params['enabled'] = ENROL_INSTANCE_ENABLED;
733 $params['now1'] = $params['now2'] = time();
735 if ($sorttimeaccess) {
736 $params['userid2'] = $USER->id;
737 $timeaccessselect = ', ul.timeaccess as lastaccessed';
738 $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
742 // When including non-enrolled but accessible courses...
743 if ($allaccessible) {
744 if (is_siteadmin()) {
745 // Site admins can access all courses.
746 $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
747 } else {
748 // If we used the enrolment as well, then this will be UNIONed.
749 if ($courseidsql) {
750 $courseidsql .= " UNION ";
753 // Include courses with guest access and no password.
754 $courseidsql .= "
755 SELECT DISTINCT e.courseid
756 FROM {enrol} e
757 WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
758 $params['emptypass'] = '';
759 $params['enabled2'] = ENROL_INSTANCE_ENABLED;
761 // Include courses where the current user is currently using guest access (may include
762 // those which require a password).
763 $courseids = [];
764 $accessdata = get_user_accessdata($USER->id);
765 foreach ($accessdata['ra'] as $contextpath => $roles) {
766 if (array_key_exists($CFG->guestroleid, $roles)) {
767 // Work out the course id from context path.
768 $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
769 if ($context instanceof context_course) {
770 $courseids[$context->instanceid] = true;
775 // Include courses where the current user has moodle/course:view capability.
776 $courses = get_user_capability_course('moodle/course:view', null, false);
777 if (!$courses) {
778 $courses = [];
780 foreach ($courses as $course) {
781 $courseids[$course->id] = true;
784 // If there are any in either category, list them individually.
785 if ($courseids) {
786 list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
787 array_keys($courseids), SQL_PARAMS_NAMED);
788 $courseidsql .= "
789 UNION
790 SELECT DISTINCT c3.id AS courseid
791 FROM {course} c3
792 WHERE c3.id $allowedsql";
793 $params = array_merge($params, $allowedparams);
798 // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
799 // we have the subselect there.
800 $sql = "SELECT $coursefields $ccselect $timeaccessselect
801 FROM {course} c
802 JOIN ($courseidsql) en ON (en.courseid = c.id)
803 $timeaccessjoin
804 $ccjoin
805 WHERE $wheres
806 $orderby";
808 $courses = $DB->get_records_sql($sql, $params, $offset, $limit);
810 // preload contexts and check visibility
811 foreach ($courses as $id=>$course) {
812 context_helper::preload_from_record($course);
813 if (!$course->visible) {
814 if (!$context = context_course::instance($id, IGNORE_MISSING)) {
815 unset($courses[$id]);
816 continue;
818 if (!has_capability('moodle/course:viewhiddencourses', $context)) {
819 unset($courses[$id]);
820 continue;
823 $courses[$id] = $course;
826 //wow! Is that really all? :-D
828 return $courses;
832 * Returns course enrolment information icons.
834 * @param object $course
835 * @param array $instances enrol instances of this course, improves performance
836 * @return array of pix_icon
838 function enrol_get_course_info_icons($course, array $instances = NULL) {
839 $icons = array();
840 if (is_null($instances)) {
841 $instances = enrol_get_instances($course->id, true);
843 $plugins = enrol_get_plugins(true);
844 foreach ($plugins as $name => $plugin) {
845 $pis = array();
846 foreach ($instances as $instance) {
847 if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
848 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
849 continue;
851 if ($instance->enrol == $name) {
852 $pis[$instance->id] = $instance;
855 if ($pis) {
856 $icons = array_merge($icons, $plugin->get_info_icons($pis));
859 return $icons;
863 * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
865 * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
866 * @return string SQL ORDER arguments.
868 function enrol_get_courses_sortingsql($sort = null) {
869 global $CFG;
871 // Prepare the visible SQL fragment as empty.
872 $visible = '';
873 // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
874 if ($sort === null || strpos($sort, 'visible') === false) {
875 // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
876 // courses to the end of the course list.
877 if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
878 $visible = 'visible DESC, ';
882 // Only create a sortorder SQL fragment if the caller didn't already pass one.
883 if ($sort === null) {
884 // If the admin has configured a course sort order, we will use this.
885 if (!empty($CFG->navsortmycoursessort)) {
886 $sort = $CFG->navsortmycoursessort . ' ASC';
888 // Otherwise we will fall back to the sortorder sorting.
889 } else {
890 $sort = 'sortorder ASC';
894 return $visible . $sort;
898 * Returns course enrolment detailed information.
900 * @param object $course
901 * @return array of html fragments - can be used to construct lists
903 function enrol_get_course_description_texts($course) {
904 $lines = array();
905 $instances = enrol_get_instances($course->id, true);
906 $plugins = enrol_get_plugins(true);
907 foreach ($instances as $instance) {
908 if (!isset($plugins[$instance->enrol])) {
909 //weird
910 continue;
912 $plugin = $plugins[$instance->enrol];
913 $text = $plugin->get_description_text($instance);
914 if ($text !== NULL) {
915 $lines[] = $text;
918 return $lines;
922 * Returns list of courses user is enrolled into.
924 * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
926 * The $fields param is a list of field names to ADD so name just the fields you really need,
927 * which will be added and uniq'd.
929 * @param int $userid User whose courses are returned, defaults to the current user.
930 * @param bool $onlyactive Return only active enrolments in courses user may see.
931 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
932 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
933 * @return array
935 function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
936 global $DB;
938 $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
940 // preload contexts and check visibility
941 if ($onlyactive) {
942 foreach ($courses as $id=>$course) {
943 context_helper::preload_from_record($course);
944 if (!$course->visible) {
945 if (!$context = context_course::instance($id)) {
946 unset($courses[$id]);
947 continue;
949 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
950 unset($courses[$id]);
951 continue;
957 return $courses;
961 * Returns list of roles per users into course.
963 * @param int $courseid Course id.
964 * @return array Array[$userid][$roleid] = role_assignment.
966 function enrol_get_course_users_roles(int $courseid) : array {
967 global $DB;
969 $context = context_course::instance($courseid);
971 $roles = array();
973 $records = $DB->get_recordset('role_assignments', array('contextid' => $context->id));
974 foreach ($records as $record) {
975 if (isset($roles[$record->userid]) === false) {
976 $roles[$record->userid] = array();
978 $roles[$record->userid][$record->roleid] = $record;
980 $records->close();
982 return $roles;
986 * Can user access at least one enrolled course?
988 * Cheat if necessary, but find out as fast as possible!
990 * @param int|stdClass $user null means use current user
991 * @return bool
993 function enrol_user_sees_own_courses($user = null) {
994 global $USER;
996 if ($user === null) {
997 $user = $USER;
999 $userid = is_object($user) ? $user->id : $user;
1001 // Guest account does not have any courses
1002 if (isguestuser($userid) or empty($userid)) {
1003 return false;
1006 // Let's cheat here if this is the current user,
1007 // if user accessed any course recently, then most probably
1008 // we do not need to query the database at all.
1009 if ($USER->id == $userid) {
1010 if (!empty($USER->enrol['enrolled'])) {
1011 foreach ($USER->enrol['enrolled'] as $until) {
1012 if ($until > time()) {
1013 return true;
1019 // Now the slow way.
1020 $courses = enrol_get_all_users_courses($userid, true);
1021 foreach($courses as $course) {
1022 if ($course->visible) {
1023 return true;
1025 context_helper::preload_from_record($course);
1026 $context = context_course::instance($course->id);
1027 if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
1028 return true;
1032 return false;
1036 * Returns list of courses user is enrolled into without performing any capability checks.
1038 * The $fields param is a list of field names to ADD so name just the fields you really need,
1039 * which will be added and uniq'd.
1041 * @param int $userid User whose courses are returned, defaults to the current user.
1042 * @param bool $onlyactive Return only active enrolments in courses user may see.
1043 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
1044 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
1045 * @return array
1047 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
1048 global $DB;
1050 // Re-Arrange the course sorting according to the admin settings.
1051 $sort = enrol_get_courses_sortingsql($sort);
1053 // Guest account does not have any courses
1054 if (isguestuser($userid) or empty($userid)) {
1055 return(array());
1058 $basefields = array('id', 'category', 'sortorder',
1059 'shortname', 'fullname', 'idnumber',
1060 'startdate', 'visible',
1061 'defaultgroupingid',
1062 'groupmode', 'groupmodeforce');
1064 if (empty($fields)) {
1065 $fields = $basefields;
1066 } else if (is_string($fields)) {
1067 // turn the fields from a string to an array
1068 $fields = explode(',', $fields);
1069 $fields = array_map('trim', $fields);
1070 $fields = array_unique(array_merge($basefields, $fields));
1071 } else if (is_array($fields)) {
1072 $fields = array_unique(array_merge($basefields, $fields));
1073 } else {
1074 throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
1076 if (in_array('*', $fields)) {
1077 $fields = array('*');
1080 $orderby = "";
1081 $sort = trim($sort);
1082 if (!empty($sort)) {
1083 $rawsorts = explode(',', $sort);
1084 $sorts = array();
1085 foreach ($rawsorts as $rawsort) {
1086 $rawsort = trim($rawsort);
1087 if (strpos($rawsort, 'c.') === 0) {
1088 $rawsort = substr($rawsort, 2);
1090 $sorts[] = trim($rawsort);
1092 $sort = 'c.'.implode(',c.', $sorts);
1093 $orderby = "ORDER BY $sort";
1096 $params = [];
1098 if ($onlyactive) {
1099 $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
1100 $params['now1'] = round(time(), -2); // improves db caching
1101 $params['now2'] = $params['now1'];
1102 $params['active'] = ENROL_USER_ACTIVE;
1103 $params['enabled'] = ENROL_INSTANCE_ENABLED;
1104 } else {
1105 $subwhere = "";
1108 $coursefields = 'c.' .join(',c.', $fields);
1109 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1110 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1111 $params['contextlevel'] = CONTEXT_COURSE;
1113 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
1114 $sql = "SELECT $coursefields $ccselect
1115 FROM {course} c
1116 JOIN (SELECT DISTINCT e.courseid
1117 FROM {enrol} e
1118 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1119 $subwhere
1120 ) en ON (en.courseid = c.id)
1121 $ccjoin
1122 WHERE c.id <> " . SITEID . "
1123 $orderby";
1124 $params['userid'] = $userid;
1126 $courses = $DB->get_records_sql($sql, $params);
1128 return $courses;
1134 * Called when user is about to be deleted.
1135 * @param object $user
1136 * @return void
1138 function enrol_user_delete($user) {
1139 global $DB;
1141 $plugins = enrol_get_plugins(true);
1142 foreach ($plugins as $plugin) {
1143 $plugin->user_delete($user);
1146 // force cleanup of all broken enrolments
1147 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1151 * Called when course is about to be deleted.
1152 * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
1153 * otherwise all enrolments in the course will be removed.
1155 * @param stdClass $course
1156 * @param int|null $userid
1157 * @return void
1159 function enrol_course_delete($course, $userid = null) {
1160 global $DB;
1162 $context = context_course::instance($course->id);
1163 $instances = enrol_get_instances($course->id, false);
1164 $plugins = enrol_get_plugins(true);
1166 if ($userid) {
1167 // If the user id is present, include only course enrolment instances which allow manual unenrolment and
1168 // the given user have a capability to perform unenrolment.
1169 $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
1170 $unenrolcap = "enrol/{$instance->enrol}:unenrol";
1171 return $plugins[$instance->enrol]->allow_unenrol($instance) &&
1172 has_capability($unenrolcap, $context, $userid);
1176 foreach ($instances as $instance) {
1177 if (isset($plugins[$instance->enrol])) {
1178 $plugins[$instance->enrol]->delete_instance($instance);
1180 // low level delete in case plugin did not do it
1181 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1182 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1183 $DB->delete_records('enrol', array('id'=>$instance->id));
1188 * Try to enrol user via default internal auth plugin.
1190 * For now this is always using the manual enrol plugin...
1192 * @param $courseid
1193 * @param $userid
1194 * @param $roleid
1195 * @param $timestart
1196 * @param $timeend
1197 * @return bool success
1199 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1200 global $DB;
1202 //note: this is hardcoded to manual plugin for now
1204 if (!enrol_is_enabled('manual')) {
1205 return false;
1208 if (!$enrol = enrol_get_plugin('manual')) {
1209 return false;
1211 if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1212 return false;
1215 if ($roleid && !$DB->record_exists('role', ['id' => $roleid])) {
1216 return false;
1219 $instance = reset($instances);
1220 $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1222 return true;
1226 * Is there a chance users might self enrol
1227 * @param int $courseid
1228 * @return bool
1230 function enrol_selfenrol_available($courseid) {
1231 $result = false;
1233 $plugins = enrol_get_plugins(true);
1234 $enrolinstances = enrol_get_instances($courseid, true);
1235 foreach($enrolinstances as $instance) {
1236 if (!isset($plugins[$instance->enrol])) {
1237 continue;
1239 if ($instance->enrol === 'guest') {
1240 continue;
1242 if ((isguestuser() || !isloggedin()) &&
1243 ($plugins[$instance->enrol]->is_self_enrol_available($instance) === true)) {
1244 $result = true;
1245 break;
1247 if ($plugins[$instance->enrol]->show_enrolme_link($instance) === true) {
1248 $result = true;
1249 break;
1253 return $result;
1257 * This function returns the end of current active user enrolment.
1259 * It deals correctly with multiple overlapping user enrolments.
1261 * @param int $courseid
1262 * @param int $userid
1263 * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1265 function enrol_get_enrolment_end($courseid, $userid) {
1266 global $DB;
1268 $sql = "SELECT ue.*
1269 FROM {user_enrolments} ue
1270 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1271 JOIN {user} u ON u.id = ue.userid
1272 WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1273 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1275 if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1276 return false;
1279 $changes = array();
1281 foreach ($enrolments as $ue) {
1282 $start = (int)$ue->timestart;
1283 $end = (int)$ue->timeend;
1284 if ($end != 0 and $end < $start) {
1285 debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1286 continue;
1288 if (isset($changes[$start])) {
1289 $changes[$start] = $changes[$start] + 1;
1290 } else {
1291 $changes[$start] = 1;
1293 if ($end === 0) {
1294 // no end
1295 } else if (isset($changes[$end])) {
1296 $changes[$end] = $changes[$end] - 1;
1297 } else {
1298 $changes[$end] = -1;
1302 // let's sort then enrolment starts&ends and go through them chronologically,
1303 // looking for current status and the next future end of enrolment
1304 ksort($changes);
1306 $now = time();
1307 $current = 0;
1308 $present = null;
1310 foreach ($changes as $time => $change) {
1311 if ($time > $now) {
1312 if ($present === null) {
1313 // we have just went past current time
1314 $present = $current;
1315 if ($present < 1) {
1316 // no enrolment active
1317 return false;
1320 if ($present !== null) {
1321 // we are already in the future - look for possible end
1322 if ($current + $change < 1) {
1323 return $time;
1327 $current += $change;
1330 if ($current > 0) {
1331 return 0;
1332 } else {
1333 return false;
1338 * Is current user accessing course via this enrolment method?
1340 * This is intended for operations that are going to affect enrol instances.
1342 * @param stdClass $instance enrol instance
1343 * @return bool
1345 function enrol_accessing_via_instance(stdClass $instance) {
1346 global $DB, $USER;
1348 if (empty($instance->id)) {
1349 return false;
1352 if (is_siteadmin()) {
1353 // Admins may go anywhere.
1354 return false;
1357 return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1361 * Returns true if user is enrolled (is participating) in course
1362 * this is intended for students and teachers.
1364 * Since 2.2 the result for active enrolments and current user are cached.
1366 * @param context $context
1367 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1368 * @param string $withcapability extra capability name
1369 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1370 * @return bool
1372 function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1373 global $USER, $DB;
1375 // First find the course context.
1376 $coursecontext = $context->get_course_context();
1378 // Make sure there is a real user specified.
1379 if ($user === null) {
1380 $userid = isset($USER->id) ? $USER->id : 0;
1381 } else {
1382 $userid = is_object($user) ? $user->id : $user;
1385 if (empty($userid)) {
1386 // Not-logged-in!
1387 return false;
1388 } else if (isguestuser($userid)) {
1389 // Guest account can not be enrolled anywhere.
1390 return false;
1393 // Note everybody participates on frontpage, so for other contexts...
1394 if ($coursecontext->instanceid != SITEID) {
1395 // Try cached info first - the enrolled flag is set only when active enrolment present.
1396 if ($USER->id == $userid) {
1397 $coursecontext->reload_if_dirty();
1398 if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1399 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1400 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1401 return false;
1403 return true;
1408 if ($onlyactive) {
1409 // Look for active enrolments only.
1410 $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1412 if ($until === false) {
1413 return false;
1416 if ($USER->id == $userid) {
1417 if ($until == 0) {
1418 $until = ENROL_MAX_TIMESTAMP;
1420 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1421 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1422 unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1423 remove_temp_course_roles($coursecontext);
1427 } else {
1428 // Any enrolment is good for us here, even outdated, disabled or inactive.
1429 $sql = "SELECT 'x'
1430 FROM {user_enrolments} ue
1431 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1432 JOIN {user} u ON u.id = ue.userid
1433 WHERE ue.userid = :userid AND u.deleted = 0";
1434 $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1435 if (!$DB->record_exists_sql($sql, $params)) {
1436 return false;
1441 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1442 return false;
1445 return true;
1449 * Returns an array of joins, wheres and params that will limit the group of
1450 * users to only those enrolled and with given capability (if specified).
1452 * Note this join will return duplicate rows for users who have been enrolled
1453 * several times (e.g. as manual enrolment, and as self enrolment). You may
1454 * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1456 * In case is guaranteed some of the joins never match any rows, the resulting
1457 * join_sql->cannotmatchanyrows will be true. This happens when the capability
1458 * is prohibited.
1460 * @param context $context
1461 * @param string $prefix optional, a prefix to the user id column
1462 * @param string|array $capability optional, may include a capability name, or array of names.
1463 * If an array is provided then this is the equivalent of a logical 'OR',
1464 * i.e. the user needs to have one of these capabilities.
1465 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1466 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1467 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1468 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1469 * @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
1471 function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $groupids = 0,
1472 $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1473 $uid = $prefix . 'u.id';
1474 $joins = array();
1475 $wheres = array();
1476 $cannotmatchanyrows = false;
1478 $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1479 $joins[] = $enrolledjoin->joins;
1480 $wheres[] = $enrolledjoin->wheres;
1481 $params = $enrolledjoin->params;
1482 $cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
1484 if (!empty($capability)) {
1485 $capjoin = get_with_capability_join($context, $capability, $uid);
1486 $joins[] = $capjoin->joins;
1487 $wheres[] = $capjoin->wheres;
1488 $params = array_merge($params, $capjoin->params);
1489 $cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
1492 if ($groupids) {
1493 $groupjoin = groups_get_members_join($groupids, $uid, $context);
1494 $joins[] = $groupjoin->joins;
1495 $params = array_merge($params, $groupjoin->params);
1496 if (!empty($groupjoin->wheres)) {
1497 $wheres[] = $groupjoin->wheres;
1499 $cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
1502 $joins = implode("\n", $joins);
1503 $wheres[] = "{$prefix}u.deleted = 0";
1504 $wheres = implode(" AND ", $wheres);
1506 return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
1510 * Returns array with sql code and parameters returning all ids
1511 * of users enrolled into course.
1513 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1515 * @param context $context
1516 * @param string $withcapability
1517 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1518 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1519 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1520 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1521 * @return array list($sql, $params)
1523 function get_enrolled_sql(context $context, $withcapability = '', $groupids = 0, $onlyactive = false, $onlysuspended = false,
1524 $enrolid = 0) {
1526 // Use unique prefix just in case somebody makes some SQL magic with the result.
1527 static $i = 0;
1528 $i++;
1529 $prefix = 'eu' . $i . '_';
1531 $capjoin = get_enrolled_with_capabilities_join(
1532 $context, $prefix, $withcapability, $groupids, $onlyactive, $onlysuspended, $enrolid);
1534 $sql = "SELECT DISTINCT {$prefix}u.id
1535 FROM {user} {$prefix}u
1536 $capjoin->joins
1537 WHERE $capjoin->wheres";
1539 return array($sql, $capjoin->params);
1543 * Returns array with sql joins and parameters returning all ids
1544 * of users enrolled into course.
1546 * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1548 * @throws coding_exception
1550 * @param context $context
1551 * @param string $useridcolumn User id column used the calling query, e.g. u.id
1552 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1553 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1554 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1555 * @return \core\dml\sql_join Contains joins, wheres, params
1557 function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1558 // Use unique prefix just in case somebody makes some SQL magic with the result.
1559 static $i = 0;
1560 $i++;
1561 $prefix = 'ej' . $i . '_';
1563 // First find the course context.
1564 $coursecontext = $context->get_course_context();
1566 $isfrontpage = ($coursecontext->instanceid == SITEID);
1568 if ($onlyactive && $onlysuspended) {
1569 throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1571 if ($isfrontpage && $onlysuspended) {
1572 throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1575 $joins = array();
1576 $wheres = array();
1577 $params = array();
1579 $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1581 // Note all users are "enrolled" on the frontpage, but for others...
1582 if (!$isfrontpage) {
1583 $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1584 $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1586 $enrolconditions = array(
1587 "{$prefix}e.id = {$prefix}ue.enrolid",
1588 "{$prefix}e.courseid = :{$prefix}courseid",
1590 if ($enrolid) {
1591 $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1592 $params[$prefix . 'enrolid'] = $enrolid;
1594 $enrolconditionssql = implode(" AND ", $enrolconditions);
1595 $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1597 $params[$prefix.'courseid'] = $coursecontext->instanceid;
1599 if (!$onlysuspended) {
1600 $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1601 $joins[] = $ejoin;
1602 if ($onlyactive) {
1603 $wheres[] = "$where1 AND $where2";
1605 } else {
1606 // Suspended only where there is enrolment but ALL are suspended.
1607 // Consider multiple enrols where one is not suspended or plain role_assign.
1608 $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1609 $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1610 $enrolconditions = array(
1611 "{$prefix}e1.id = {$prefix}ue1.enrolid",
1612 "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1614 if ($enrolid) {
1615 $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1616 $params[$prefix . 'e1_enrolid'] = $enrolid;
1618 $enrolconditionssql = implode(" AND ", $enrolconditions);
1619 $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1620 $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1621 $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1624 if ($onlyactive || $onlysuspended) {
1625 $now = round(time(), -2); // Rounding helps caching in DB.
1626 $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1627 $prefix . 'active' => ENROL_USER_ACTIVE,
1628 $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1632 $joins = implode("\n", $joins);
1633 $wheres = implode(" AND ", $wheres);
1635 return new \core\dml\sql_join($joins, $wheres, $params);
1639 * Returns list of users enrolled into course.
1641 * @param context $context
1642 * @param string $withcapability
1643 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1644 * @param string $userfields requested user record fields
1645 * @param string $orderby
1646 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1647 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1648 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1649 * @return array of user records
1651 function get_enrolled_users(context $context, $withcapability = '', $groupids = 0, $userfields = 'u.*', $orderby = null,
1652 $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1653 global $DB;
1655 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupids, $onlyactive);
1656 $sql = "SELECT $userfields
1657 FROM {user} u
1658 JOIN ($esql) je ON je.id = u.id
1659 WHERE u.deleted = 0";
1661 if ($orderby) {
1662 $sql = "$sql ORDER BY $orderby";
1663 } else {
1664 list($sort, $sortparams) = users_order_by_sql('u');
1665 $sql = "$sql ORDER BY $sort";
1666 $params = array_merge($params, $sortparams);
1669 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1673 * Counts list of users enrolled into course (as per above function)
1675 * @param context $context
1676 * @param string $withcapability
1677 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1678 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1679 * @return int number of users enrolled into course
1681 function count_enrolled_users(context $context, $withcapability = '', $groupids = 0, $onlyactive = false) {
1682 global $DB;
1684 $capjoin = get_enrolled_with_capabilities_join(
1685 $context, '', $withcapability, $groupids, $onlyactive);
1687 $sql = "SELECT COUNT(DISTINCT u.id)
1688 FROM {user} u
1689 $capjoin->joins
1690 WHERE $capjoin->wheres AND u.deleted = 0";
1692 return $DB->count_records_sql($sql, $capjoin->params);
1696 * Send welcome email "from" options.
1698 * @return array list of from options
1700 function enrol_send_welcome_email_options() {
1701 return [
1702 ENROL_DO_NOT_SEND_EMAIL => get_string('no'),
1703 ENROL_SEND_EMAIL_FROM_COURSE_CONTACT => get_string('sendfromcoursecontact', 'enrol'),
1704 ENROL_SEND_EMAIL_FROM_KEY_HOLDER => get_string('sendfromkeyholder', 'enrol'),
1705 ENROL_SEND_EMAIL_FROM_NOREPLY => get_string('sendfromnoreply', 'enrol')
1710 * Serve the user enrolment form as a fragment.
1712 * @param array $args List of named arguments for the fragment loader.
1713 * @return string
1715 function enrol_output_fragment_user_enrolment_form($args) {
1716 global $CFG, $DB;
1718 $args = (object) $args;
1719 $context = $args->context;
1720 require_capability('moodle/course:enrolreview', $context);
1722 $ueid = $args->ueid;
1723 $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1724 $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1725 $plugin = enrol_get_plugin($instance->enrol);
1726 $customdata = [
1727 'ue' => $userenrolment,
1728 'modal' => true,
1729 'enrolinstancename' => $plugin->get_instance_name($instance)
1732 // Set the data if applicable.
1733 $data = [];
1734 if (isset($args->formdata)) {
1735 $serialiseddata = json_decode($args->formdata);
1736 parse_str($serialiseddata, $data);
1739 require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1740 $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1742 if (!empty($data)) {
1743 $mform->set_data($data);
1744 $mform->is_validated();
1747 return $mform->render();
1751 * Returns the course where a user enrolment belong to.
1753 * @param int $ueid user_enrolments id
1754 * @return stdClass
1756 function enrol_get_course_by_user_enrolment_id($ueid) {
1757 global $DB;
1758 $sql = "SELECT c.* FROM {user_enrolments} ue
1759 JOIN {enrol} e ON e.id = ue.enrolid
1760 JOIN {course} c ON c.id = e.courseid
1761 WHERE ue.id = :ueid";
1762 return $DB->get_record_sql($sql, array('ueid' => $ueid));
1766 * Return all users enrolled in a course.
1768 * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1769 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1770 * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1771 * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1772 * @return stdClass[]
1774 function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
1775 global $DB;
1777 if (!$courseid && !$usersfilter && !$uefilter) {
1778 throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1781 $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1782 ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1783 ue.timemodified AS uetimemodified, e.status AS estatus,
1784 u.* FROM {user_enrolments} ue
1785 JOIN {enrol} e ON e.id = ue.enrolid
1786 JOIN {user} u ON ue.userid = u.id
1787 WHERE ";
1788 $params = array();
1790 if ($courseid) {
1791 $conditions[] = "e.courseid = :courseid";
1792 $params['courseid'] = $courseid;
1795 if ($onlyactive) {
1796 $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1797 "(ue.timeend = 0 OR ue.timeend > :now2)";
1798 // Improves db caching.
1799 $params['now1'] = round(time(), -2);
1800 $params['now2'] = $params['now1'];
1801 $params['active'] = ENROL_USER_ACTIVE;
1802 $params['enabled'] = ENROL_INSTANCE_ENABLED;
1805 if ($usersfilter) {
1806 list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1807 $conditions[] = "ue.userid $usersql";
1808 $params = $params + $userparams;
1811 if ($uefilter) {
1812 list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1813 $conditions[] = "ue.id $uesql";
1814 $params = $params + $ueparams;
1817 return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1821 * Get the list of options for the enrolment period dropdown
1823 * @return array List of options for the enrolment period dropdown
1825 function enrol_get_period_list() {
1826 $periodmenu = [];
1827 $periodmenu[''] = get_string('unlimited');
1828 for ($i = 1; $i <= 365; $i++) {
1829 $seconds = $i * DAYSECS;
1830 $periodmenu[$seconds] = get_string('numdays', '', $i);
1832 return $periodmenu;
1836 * Calculate duration base on start time and end time
1838 * @param int $timestart Time start
1839 * @param int $timeend Time end
1840 * @return float|int Calculated duration
1842 function enrol_calculate_duration($timestart, $timeend) {
1843 $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1844 return $duration;
1848 * Enrolment plugins abstract class.
1850 * All enrol plugins should be based on this class,
1851 * this is also the main source of documentation.
1853 * @copyright 2010 Petr Skoda {@link http://skodak.org}
1854 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1856 abstract class enrol_plugin {
1857 protected $config = null;
1860 * Returns name of this enrol plugin
1861 * @return string
1863 public function get_name() {
1864 // second word in class is always enrol name, sorry, no fancy plugin names with _
1865 $words = explode('_', get_class($this));
1866 return $words[1];
1870 * Returns localised name of enrol instance
1872 * @param object $instance (null is accepted too)
1873 * @return string
1875 public function get_instance_name($instance) {
1876 if (empty($instance->name)) {
1877 $enrol = $this->get_name();
1878 return get_string('pluginname', 'enrol_'.$enrol);
1879 } else {
1880 $context = context_course::instance($instance->courseid);
1881 return format_string($instance->name, true, array('context'=>$context));
1886 * Returns optional enrolment information icons.
1888 * This is used in course list for quick overview of enrolment options.
1890 * We are not using single instance parameter because sometimes
1891 * we might want to prevent icon repetition when multiple instances
1892 * of one type exist. One instance may also produce several icons.
1894 * @param array $instances all enrol instances of this type in one course
1895 * @return array of pix_icon
1897 public function get_info_icons(array $instances) {
1898 return array();
1902 * Returns optional enrolment instance description text.
1904 * This is used in detailed course information.
1907 * @param object $instance
1908 * @return string short html text
1910 public function get_description_text($instance) {
1911 return null;
1915 * Makes sure config is loaded and cached.
1916 * @return void
1918 protected function load_config() {
1919 if (!isset($this->config)) {
1920 $name = $this->get_name();
1921 $this->config = get_config("enrol_$name");
1926 * Returns plugin config value
1927 * @param string $name
1928 * @param string $default value if config does not exist yet
1929 * @return string value or default
1931 public function get_config($name, $default = NULL) {
1932 $this->load_config();
1933 return isset($this->config->$name) ? $this->config->$name : $default;
1937 * Sets plugin config value
1938 * @param string $name name of config
1939 * @param string $value string config value, null means delete
1940 * @return string value
1942 public function set_config($name, $value) {
1943 $pluginname = $this->get_name();
1944 $this->load_config();
1945 if ($value === NULL) {
1946 unset($this->config->$name);
1947 } else {
1948 $this->config->$name = $value;
1950 set_config($name, $value, "enrol_$pluginname");
1954 * Does this plugin assign protected roles are can they be manually removed?
1955 * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1957 public function roles_protected() {
1958 return true;
1962 * Does this plugin allow manual enrolments?
1964 * @param stdClass $instance course enrol instance
1965 * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1967 * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1969 public function allow_enrol(stdClass $instance) {
1970 return false;
1974 * Does this plugin allow manual unenrolment of all users?
1975 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1977 * @param stdClass $instance course enrol instance
1978 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1980 public function allow_unenrol(stdClass $instance) {
1981 return false;
1985 * Does this plugin allow manual unenrolment of a specific user?
1986 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1988 * This is useful especially for synchronisation plugins that
1989 * do suspend instead of full unenrolment.
1991 * @param stdClass $instance course enrol instance
1992 * @param stdClass $ue record from user_enrolments table, specifies user
1994 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1996 public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1997 return $this->allow_unenrol($instance);
2001 * Does this plugin allow manual changes in user_enrolments table?
2003 * All plugins allowing this must implement 'enrol/xxx:manage' capability
2005 * @param stdClass $instance course enrol instance
2006 * @return bool - true means it is possible to change enrol period and status in user_enrolments table
2008 public function allow_manage(stdClass $instance) {
2009 return false;
2013 * Does this plugin support some way to user to self enrol?
2015 * @param stdClass $instance course enrol instance
2017 * @return bool - true means show "Enrol me in this course" link in course UI
2019 public function show_enrolme_link(stdClass $instance) {
2020 return false;
2024 * Does this plugin support some way to self enrol?
2025 * This function doesn't check user capabilities. Use can_self_enrol to check capabilities.
2027 * @param stdClass $instance enrolment instance
2028 * @return bool - true means "Enrol me in this course" link could be available.
2030 public function is_self_enrol_available(stdClass $instance) {
2031 return false;
2035 * Attempt to automatically enrol current user in course without any interaction,
2036 * calling code has to make sure the plugin and instance are active.
2038 * This should return either a timestamp in the future or false.
2040 * @param stdClass $instance course enrol instance
2041 * @return bool|int false means not enrolled, integer means timeend
2043 public function try_autoenrol(stdClass $instance) {
2044 global $USER;
2046 return false;
2050 * Attempt to automatically gain temporary guest access to course,
2051 * calling code has to make sure the plugin and instance are active.
2053 * This should return either a timestamp in the future or false.
2055 * @param stdClass $instance course enrol instance
2056 * @return bool|int false means no guest access, integer means timeend
2058 public function try_guestaccess(stdClass $instance) {
2059 global $USER;
2061 return false;
2065 * Enrol user into course via enrol instance.
2067 * @param stdClass $instance
2068 * @param int $userid
2069 * @param int $roleid optional role id
2070 * @param int $timestart 0 means unknown
2071 * @param int $timeend 0 means forever
2072 * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
2073 * @param bool $recovergrades restore grade history
2074 * @return void
2076 public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
2077 global $DB, $USER, $CFG; // CFG necessary!!!
2079 if ($instance->courseid == SITEID) {
2080 throw new coding_exception('invalid attempt to enrol into frontpage course!');
2083 $name = $this->get_name();
2084 $courseid = $instance->courseid;
2086 if ($instance->enrol !== $name) {
2087 throw new coding_exception('invalid enrol instance!');
2089 $context = context_course::instance($instance->courseid, MUST_EXIST);
2090 if (!isset($recovergrades)) {
2091 $recovergrades = $CFG->recovergradesdefault;
2094 $inserted = false;
2095 $updated = false;
2096 if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2097 //only update if timestart or timeend or status are different.
2098 if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
2099 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
2101 } else {
2102 $ue = new stdClass();
2103 $ue->enrolid = $instance->id;
2104 $ue->status = is_null($status) ? ENROL_USER_ACTIVE : $status;
2105 $ue->userid = $userid;
2106 $ue->timestart = $timestart;
2107 $ue->timeend = $timeend;
2108 $ue->modifierid = $USER->id;
2109 $ue->timecreated = time();
2110 $ue->timemodified = $ue->timecreated;
2111 $ue->id = $DB->insert_record('user_enrolments', $ue);
2113 $inserted = true;
2116 if ($inserted) {
2117 // Trigger event.
2118 $event = \core\event\user_enrolment_created::create(
2119 array(
2120 'objectid' => $ue->id,
2121 'courseid' => $courseid,
2122 'context' => $context,
2123 'relateduserid' => $ue->userid,
2124 'other' => array('enrol' => $name)
2127 $event->trigger();
2128 // Check if course contacts cache needs to be cleared.
2129 core_course_category::user_enrolment_changed($courseid, $ue->userid,
2130 $ue->status, $ue->timestart, $ue->timeend);
2133 if ($roleid) {
2134 // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2135 if ($this->roles_protected()) {
2136 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2137 } else {
2138 role_assign($roleid, $userid, $context->id);
2142 // Recover old grades if present.
2143 if ($recovergrades) {
2144 require_once("$CFG->libdir/gradelib.php");
2145 grade_recover_history_grades($userid, $courseid);
2148 // reset current user enrolment caching
2149 if ($userid == $USER->id) {
2150 if (isset($USER->enrol['enrolled'][$courseid])) {
2151 unset($USER->enrol['enrolled'][$courseid]);
2153 if (isset($USER->enrol['tempguest'][$courseid])) {
2154 unset($USER->enrol['tempguest'][$courseid]);
2155 remove_temp_course_roles($context);
2161 * Store user_enrolments changes and trigger event.
2163 * @param stdClass $instance
2164 * @param int $userid
2165 * @param int $status
2166 * @param int $timestart
2167 * @param int $timeend
2168 * @return void
2170 public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2171 global $DB, $USER, $CFG;
2173 $name = $this->get_name();
2175 if ($instance->enrol !== $name) {
2176 throw new coding_exception('invalid enrol instance!');
2179 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2180 // weird, user not enrolled
2181 return;
2184 $modified = false;
2185 if (isset($status) and $ue->status != $status) {
2186 $ue->status = $status;
2187 $modified = true;
2189 if (isset($timestart) and $ue->timestart != $timestart) {
2190 $ue->timestart = $timestart;
2191 $modified = true;
2193 if (isset($timeend) and $ue->timeend != $timeend) {
2194 $ue->timeend = $timeend;
2195 $modified = true;
2198 if (!$modified) {
2199 // no change
2200 return;
2203 $ue->modifierid = $USER->id;
2204 $ue->timemodified = time();
2205 $DB->update_record('user_enrolments', $ue);
2207 // User enrolments have changed, so mark user as dirty.
2208 mark_user_dirty($userid);
2210 // Invalidate core_access cache for get_suspended_userids.
2211 cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2213 // Trigger event.
2214 $event = \core\event\user_enrolment_updated::create(
2215 array(
2216 'objectid' => $ue->id,
2217 'courseid' => $instance->courseid,
2218 'context' => context_course::instance($instance->courseid),
2219 'relateduserid' => $ue->userid,
2220 'other' => array('enrol' => $name)
2223 $event->trigger();
2225 core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2226 $ue->status, $ue->timestart, $ue->timeend);
2230 * Unenrol user from course,
2231 * the last unenrolment removes all remaining roles.
2233 * @param stdClass $instance
2234 * @param int $userid
2235 * @return void
2237 public function unenrol_user(stdClass $instance, $userid) {
2238 global $CFG, $USER, $DB;
2239 require_once("$CFG->dirroot/group/lib.php");
2241 $name = $this->get_name();
2242 $courseid = $instance->courseid;
2244 if ($instance->enrol !== $name) {
2245 throw new coding_exception('invalid enrol instance!');
2247 $context = context_course::instance($instance->courseid, MUST_EXIST);
2249 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2250 // weird, user not enrolled
2251 return;
2254 // Remove all users groups linked to this enrolment instance.
2255 if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2256 foreach ($gms as $gm) {
2257 groups_remove_member($gm->groupid, $gm->userid);
2261 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2262 $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2264 // add extra info and trigger event
2265 $ue->courseid = $courseid;
2266 $ue->enrol = $name;
2268 $sql = "SELECT 'x'
2269 FROM {user_enrolments} ue
2270 JOIN {enrol} e ON (e.id = ue.enrolid)
2271 WHERE ue.userid = :userid AND e.courseid = :courseid";
2272 if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2273 $ue->lastenrol = false;
2275 } else {
2276 // the big cleanup IS necessary!
2277 require_once("$CFG->libdir/gradelib.php");
2279 // remove all remaining roles
2280 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2282 //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2283 groups_delete_group_members($courseid, $userid);
2285 grade_user_unenrol($courseid, $userid);
2287 $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2289 $ue->lastenrol = true; // means user not enrolled any more
2291 // Trigger event.
2292 $event = \core\event\user_enrolment_deleted::create(
2293 array(
2294 'courseid' => $courseid,
2295 'context' => $context,
2296 'relateduserid' => $ue->userid,
2297 'objectid' => $ue->id,
2298 'other' => array(
2299 'userenrolment' => (array)$ue,
2300 'enrol' => $name
2304 $event->trigger();
2306 // User enrolments have changed, so mark user as dirty.
2307 mark_user_dirty($userid);
2309 // Check if courrse contacts cache needs to be cleared.
2310 core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2312 // reset current user enrolment caching
2313 if ($userid == $USER->id) {
2314 if (isset($USER->enrol['enrolled'][$courseid])) {
2315 unset($USER->enrol['enrolled'][$courseid]);
2317 if (isset($USER->enrol['tempguest'][$courseid])) {
2318 unset($USER->enrol['tempguest'][$courseid]);
2319 remove_temp_course_roles($context);
2325 * Forces synchronisation of user enrolments.
2327 * This is important especially for external enrol plugins,
2328 * this function is called for all enabled enrol plugins
2329 * right after every user login.
2331 * @param object $user user record
2332 * @return void
2334 public function sync_user_enrolments($user) {
2335 // override if necessary
2339 * This returns false for backwards compatibility, but it is really recommended.
2341 * @since Moodle 3.1
2342 * @return boolean
2344 public function use_standard_editing_ui() {
2345 return false;
2349 * Return whether or not, given the current state, it is possible to add a new instance
2350 * of this enrolment plugin to the course.
2352 * Default implementation is just for backwards compatibility.
2354 * @param int $courseid
2355 * @return boolean
2357 public function can_add_instance($courseid) {
2358 $link = $this->get_newinstance_link($courseid);
2359 return !empty($link);
2363 * Return whether or not, given the current state, it is possible to edit an instance
2364 * of this enrolment plugin in the course. Used by the standard editing UI
2365 * to generate a link to the edit instance form if editing is allowed.
2367 * @param stdClass $instance
2368 * @return boolean
2370 public function can_edit_instance($instance) {
2371 $context = context_course::instance($instance->courseid);
2373 return has_capability('enrol/' . $instance->enrol . ':config', $context);
2377 * Returns link to page which may be used to add new instance of enrolment plugin in course.
2378 * @param int $courseid
2379 * @return moodle_url page url
2381 public function get_newinstance_link($courseid) {
2382 // override for most plugins, check if instance already exists in cases only one instance is supported
2383 return NULL;
2387 * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2389 public function instance_deleteable($instance) {
2390 throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2391 enrol_plugin::can_delete_instance() instead');
2395 * Is it possible to delete enrol instance via standard UI?
2397 * @param stdClass $instance
2398 * @return bool
2400 public function can_delete_instance($instance) {
2401 return false;
2405 * Is it possible to hide/show enrol instance via standard UI?
2407 * @param stdClass $instance
2408 * @return bool
2410 public function can_hide_show_instance($instance) {
2411 debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2412 return true;
2416 * Returns link to manual enrol UI if exists.
2417 * Does the access control tests automatically.
2419 * @param object $instance
2420 * @return moodle_url
2422 public function get_manual_enrol_link($instance) {
2423 return NULL;
2427 * Returns list of unenrol links for all enrol instances in course.
2429 * @param int $instance
2430 * @return moodle_url or NULL if self unenrolment not supported
2432 public function get_unenrolself_link($instance) {
2433 global $USER, $CFG, $DB;
2435 $name = $this->get_name();
2436 if ($instance->enrol !== $name) {
2437 throw new coding_exception('invalid enrol instance!');
2440 if ($instance->courseid == SITEID) {
2441 return NULL;
2444 if (!enrol_is_enabled($name)) {
2445 return NULL;
2448 if ($instance->status != ENROL_INSTANCE_ENABLED) {
2449 return NULL;
2452 if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2453 return NULL;
2456 $context = context_course::instance($instance->courseid, MUST_EXIST);
2458 if (!has_capability("enrol/$name:unenrolself", $context)) {
2459 return NULL;
2462 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2463 return NULL;
2466 return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2470 * Adds enrol instance UI to course edit form
2472 * @param object $instance enrol instance or null if does not exist yet
2473 * @param MoodleQuickForm $mform
2474 * @param object $data
2475 * @param object $context context of existing course or parent category if course does not exist
2476 * @return void
2478 public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2479 // override - usually at least enable/disable switch, has to add own form header
2483 * Adds form elements to add/edit instance form.
2485 * @since Moodle 3.1
2486 * @param object $instance enrol instance or null if does not exist yet
2487 * @param MoodleQuickForm $mform
2488 * @param context $context
2489 * @return void
2491 public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2492 // Do nothing by default.
2496 * Perform custom validation of the data used to edit the instance.
2498 * @since Moodle 3.1
2499 * @param array $data array of ("fieldname"=>value) of submitted data
2500 * @param array $files array of uploaded files "element_name"=>tmp_file_path
2501 * @param object $instance The instance data loaded from the DB.
2502 * @param context $context The context of the instance we are editing
2503 * @return array of "element_name"=>"error_description" if there are errors,
2504 * or an empty array if everything is OK.
2506 public function edit_instance_validation($data, $files, $instance, $context) {
2507 // No errors by default.
2508 debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2509 return array();
2513 * Validates course edit form data
2515 * @param object $instance enrol instance or null if does not exist yet
2516 * @param array $data
2517 * @param object $context context of existing course or parent category if course does not exist
2518 * @return array errors array
2520 public function course_edit_validation($instance, array $data, $context) {
2521 return array();
2525 * Called after updating/inserting course.
2527 * @param bool $inserted true if course just inserted
2528 * @param object $course
2529 * @param object $data form data
2530 * @return void
2532 public function course_updated($inserted, $course, $data) {
2533 if ($inserted) {
2534 if ($this->get_config('defaultenrol')) {
2535 $this->add_default_instance($course);
2541 * Add new instance of enrol plugin.
2542 * @param object $course
2543 * @param array instance fields
2544 * @return int id of new instance, null if can not be created
2546 public function add_instance($course, array $fields = NULL) {
2547 global $DB;
2549 if ($course->id == SITEID) {
2550 throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2553 $instance = new stdClass();
2554 $instance->enrol = $this->get_name();
2555 $instance->status = ENROL_INSTANCE_ENABLED;
2556 $instance->courseid = $course->id;
2557 $instance->enrolstartdate = 0;
2558 $instance->enrolenddate = 0;
2559 $instance->timemodified = time();
2560 $instance->timecreated = $instance->timemodified;
2561 $instance->sortorder = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2563 $fields = (array)$fields;
2564 unset($fields['enrol']);
2565 unset($fields['courseid']);
2566 unset($fields['sortorder']);
2567 foreach($fields as $field=>$value) {
2568 $instance->$field = $value;
2571 $instance->id = $DB->insert_record('enrol', $instance);
2573 \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2575 return $instance->id;
2579 * Update instance of enrol plugin.
2581 * @since Moodle 3.1
2582 * @param stdClass $instance
2583 * @param stdClass $data modified instance fields
2584 * @return boolean
2586 public function update_instance($instance, $data) {
2587 global $DB;
2588 $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2589 'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2590 'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2591 'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2592 'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2593 'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2595 foreach ($properties as $key) {
2596 if (isset($data->$key)) {
2597 $instance->$key = $data->$key;
2600 $instance->timemodified = time();
2602 $update = $DB->update_record('enrol', $instance);
2603 if ($update) {
2604 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2606 return $update;
2610 * Add new instance of enrol plugin with default settings,
2611 * called when adding new instance manually or when adding new course.
2613 * Not all plugins support this.
2615 * @param object $course
2616 * @return int id of new instance or null if no default supported
2618 public function add_default_instance($course) {
2619 return null;
2623 * Update instance status
2625 * Override when plugin needs to do some action when enabled or disabled.
2627 * @param stdClass $instance
2628 * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2629 * @return void
2631 public function update_status($instance, $newstatus) {
2632 global $DB;
2634 $instance->status = $newstatus;
2635 $DB->update_record('enrol', $instance);
2637 $context = context_course::instance($instance->courseid);
2638 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2640 // Invalidate all enrol caches.
2641 $context->mark_dirty();
2645 * Delete course enrol plugin instance, unenrol all users.
2646 * @param object $instance
2647 * @return void
2649 public function delete_instance($instance) {
2650 global $DB;
2652 $name = $this->get_name();
2653 if ($instance->enrol !== $name) {
2654 throw new coding_exception('invalid enrol instance!');
2657 //first unenrol all users
2658 $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2659 foreach ($participants as $participant) {
2660 $this->unenrol_user($instance, $participant->userid);
2662 $participants->close();
2664 // now clean up all remainders that were not removed correctly
2665 if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2666 foreach ($gms as $gm) {
2667 groups_remove_member($gm->groupid, $gm->userid);
2670 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2671 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2673 // finally drop the enrol row
2674 $DB->delete_records('enrol', array('id'=>$instance->id));
2676 $context = context_course::instance($instance->courseid);
2677 \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2679 // Invalidate all enrol caches.
2680 $context->mark_dirty();
2684 * Creates course enrol form, checks if form submitted
2685 * and enrols user if necessary. It can also redirect.
2687 * @param stdClass $instance
2688 * @return string html text, usually a form in a text box
2690 public function enrol_page_hook(stdClass $instance) {
2691 return null;
2695 * Checks if user can self enrol.
2697 * @param stdClass $instance enrolment instance
2698 * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2699 * used by navigation to improve performance.
2700 * @return bool|string true if successful, else error message or false
2702 public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2703 return false;
2707 * Return information for enrolment instance containing list of parameters required
2708 * for enrolment, name of enrolment plugin etc.
2710 * @param stdClass $instance enrolment instance
2711 * @return array instance info.
2713 public function get_enrol_info(stdClass $instance) {
2714 return null;
2718 * Adds navigation links into course admin block.
2720 * By defaults looks for manage links only.
2722 * @param navigation_node $instancesnode
2723 * @param stdClass $instance
2724 * @return void
2726 public function add_course_navigation($instancesnode, stdClass $instance) {
2727 if ($this->use_standard_editing_ui()) {
2728 $context = context_course::instance($instance->courseid);
2729 $cap = 'enrol/' . $instance->enrol . ':config';
2730 if (has_capability($cap, $context)) {
2731 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2732 $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2733 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2739 * Returns edit icons for the page with list of instances
2740 * @param stdClass $instance
2741 * @return array
2743 public function get_action_icons(stdClass $instance) {
2744 global $OUTPUT;
2746 $icons = array();
2747 if ($this->use_standard_editing_ui()) {
2748 $context = context_course::instance($instance->courseid);
2749 $cap = 'enrol/' . $instance->enrol . ':config';
2750 if (has_capability($cap, $context)) {
2751 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2752 $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2753 $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2754 array('class' => 'iconsmall')));
2757 return $icons;
2761 * Reads version.php and determines if it is necessary
2762 * to execute the cron job now.
2763 * @return bool
2765 public function is_cron_required() {
2766 global $CFG;
2768 $name = $this->get_name();
2769 $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2770 $plugin = new stdClass();
2771 include($versionfile);
2772 if (empty($plugin->cron)) {
2773 return false;
2775 $lastexecuted = $this->get_config('lastcron', 0);
2776 if ($lastexecuted + $plugin->cron < time()) {
2777 return true;
2778 } else {
2779 return false;
2784 * Called for all enabled enrol plugins that returned true from is_cron_required().
2785 * @return void
2787 public function cron() {
2791 * Called when user is about to be deleted
2792 * @param object $user
2793 * @return void
2795 public function user_delete($user) {
2796 global $DB;
2798 $sql = "SELECT e.*
2799 FROM {enrol} e
2800 JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2801 WHERE e.enrol = :name AND ue.userid = :userid";
2802 $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2804 $rs = $DB->get_recordset_sql($sql, $params);
2805 foreach($rs as $instance) {
2806 $this->unenrol_user($instance, $user->id);
2808 $rs->close();
2812 * Returns an enrol_user_button that takes the user to a page where they are able to
2813 * enrol users into the managers course through this plugin.
2815 * Optional: If the plugin supports manual enrolments it can choose to override this
2816 * otherwise it shouldn't
2818 * @param course_enrolment_manager $manager
2819 * @return enrol_user_button|false
2821 public function get_manual_enrol_button(course_enrolment_manager $manager) {
2822 return false;
2826 * Gets an array of the user enrolment actions
2828 * @param course_enrolment_manager $manager
2829 * @param stdClass $ue
2830 * @return array An array of user_enrolment_actions
2832 public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2833 $actions = [];
2834 $context = $manager->get_context();
2835 $instance = $ue->enrolmentinstance;
2836 $params = $manager->get_moodlepage()->url->params();
2837 $params['ue'] = $ue->id;
2839 // Edit enrolment action.
2840 if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2841 $title = get_string('editenrolment', 'enrol');
2842 $icon = new pix_icon('t/edit', $title);
2843 $url = new moodle_url('/enrol/editenrolment.php', $params);
2844 $actionparams = [
2845 'class' => 'editenrollink',
2846 'rel' => $ue->id,
2847 'data-action' => ENROL_ACTION_EDIT
2849 $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2852 // Unenrol action.
2853 if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2854 $title = get_string('unenrol', 'enrol');
2855 $icon = new pix_icon('t/delete', $title);
2856 $url = new moodle_url('/enrol/unenroluser.php', $params);
2857 $actionparams = [
2858 'class' => 'unenrollink',
2859 'rel' => $ue->id,
2860 'data-action' => ENROL_ACTION_UNENROL
2862 $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2864 return $actions;
2868 * Returns true if the plugin has one or more bulk operations that can be performed on
2869 * user enrolments.
2871 * @param course_enrolment_manager $manager
2872 * @return bool
2874 public function has_bulk_operations(course_enrolment_manager $manager) {
2875 return false;
2879 * Return an array of enrol_bulk_enrolment_operation objects that define
2880 * the bulk actions that can be performed on user enrolments by the plugin.
2882 * @param course_enrolment_manager $manager
2883 * @return array
2885 public function get_bulk_operations(course_enrolment_manager $manager) {
2886 return array();
2890 * Do any enrolments need expiration processing.
2892 * Plugins that want to call this functionality must implement 'expiredaction' config setting.
2894 * @param progress_trace $trace
2895 * @param int $courseid one course, empty mean all
2896 * @return bool true if any data processed, false if not
2898 public function process_expirations(progress_trace $trace, $courseid = null) {
2899 global $DB;
2901 $name = $this->get_name();
2902 if (!enrol_is_enabled($name)) {
2903 $trace->finished();
2904 return false;
2907 $processed = false;
2908 $params = array();
2909 $coursesql = "";
2910 if ($courseid) {
2911 $coursesql = "AND e.courseid = :courseid";
2914 // Deal with expired accounts.
2915 $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
2917 if ($action == ENROL_EXT_REMOVED_UNENROL) {
2918 $instances = array();
2919 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2920 FROM {user_enrolments} ue
2921 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2922 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2923 WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
2924 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
2926 $rs = $DB->get_recordset_sql($sql, $params);
2927 foreach ($rs as $ue) {
2928 if (!$processed) {
2929 $trace->output("Starting processing of enrol_$name expirations...");
2930 $processed = true;
2932 if (empty($instances[$ue->enrolid])) {
2933 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2935 $instance = $instances[$ue->enrolid];
2936 if (!$this->roles_protected()) {
2937 // Let's just guess what extra roles are supposed to be removed.
2938 if ($instance->roleid) {
2939 role_unassign($instance->roleid, $ue->userid, $ue->contextid);
2942 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
2943 $this->unenrol_user($instance, $ue->userid);
2944 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
2946 $rs->close();
2947 unset($instances);
2949 } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
2950 $instances = array();
2951 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2952 FROM {user_enrolments} ue
2953 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2954 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2955 WHERE ue.timeend > 0 AND ue.timeend < :now
2956 AND ue.status = :useractive $coursesql";
2957 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
2958 $rs = $DB->get_recordset_sql($sql, $params);
2959 foreach ($rs as $ue) {
2960 if (!$processed) {
2961 $trace->output("Starting processing of enrol_$name expirations...");
2962 $processed = true;
2964 if (empty($instances[$ue->enrolid])) {
2965 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2967 $instance = $instances[$ue->enrolid];
2969 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
2970 if (!$this->roles_protected()) {
2971 // Let's just guess what roles should be removed.
2972 $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
2973 if ($count == 1) {
2974 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
2976 } else if ($count > 1 and $instance->roleid) {
2977 role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
2980 // In any case remove all roles that belong to this instance and user.
2981 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
2982 // Final cleanup of subcontexts if there are no more course roles.
2983 if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
2984 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
2988 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
2989 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
2991 $rs->close();
2992 unset($instances);
2994 } else {
2995 // ENROL_EXT_REMOVED_KEEP means no changes.
2998 if ($processed) {
2999 $trace->output("...finished processing of enrol_$name expirations");
3000 } else {
3001 $trace->output("No expired enrol_$name enrolments detected");
3003 $trace->finished();
3005 return $processed;
3009 * Send expiry notifications.
3011 * Plugin that wants to have expiry notification MUST implement following:
3012 * - expirynotifyhour plugin setting,
3013 * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
3014 * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
3015 * expirymessageenrolledsubject and expirymessageenrolledbody),
3016 * - expiry_notification provider in db/messages.php,
3017 * - upgrade code that sets default thresholds for existing courses (should be 1 day),
3018 * - something that calls this method, such as cron.
3020 * @param progress_trace $trace (accepts bool for backwards compatibility only)
3022 public function send_expiry_notifications($trace) {
3023 global $DB, $CFG;
3025 $name = $this->get_name();
3026 if (!enrol_is_enabled($name)) {
3027 $trace->finished();
3028 return;
3031 // Unfortunately this may take a long time, it should not be interrupted,
3032 // otherwise users get duplicate notification.
3034 core_php_time_limit::raise();
3035 raise_memory_limit(MEMORY_HUGE);
3038 $expirynotifylast = $this->get_config('expirynotifylast', 0);
3039 $expirynotifyhour = $this->get_config('expirynotifyhour');
3040 if (is_null($expirynotifyhour)) {
3041 debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
3042 $trace->finished();
3043 return;
3046 if (!($trace instanceof progress_trace)) {
3047 $trace = $trace ? new text_progress_trace() : new null_progress_trace();
3048 debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
3051 $timenow = time();
3052 $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
3054 if ($expirynotifylast > $notifytime) {
3055 $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
3056 $trace->finished();
3057 return;
3059 } else if ($timenow < $notifytime) {
3060 $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
3061 $trace->finished();
3062 return;
3065 $trace->output('Processing '.$name.' enrolment expiration notifications...');
3067 // Notify users responsible for enrolment once every day.
3068 $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
3069 FROM {user_enrolments} ue
3070 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
3071 JOIN {course} c ON (c.id = e.courseid)
3072 JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
3073 WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
3074 ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
3075 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
3077 $rs = $DB->get_recordset_sql($sql, $params);
3079 $lastenrollid = 0;
3080 $users = array();
3082 foreach($rs as $ue) {
3083 if ($lastenrollid and $lastenrollid != $ue->enrolid) {
3084 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3085 $users = array();
3087 $lastenrollid = $ue->enrolid;
3089 $enroller = $this->get_enroller($ue->enrolid);
3090 $context = context_course::instance($ue->courseid);
3092 $user = $DB->get_record('user', array('id'=>$ue->userid));
3094 $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
3096 if (!$ue->notifyall) {
3097 continue;
3100 if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
3101 // Notify enrolled users only once at the start of the threshold.
3102 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3103 continue;
3106 $this->notify_expiry_enrolled($user, $ue, $trace);
3108 $rs->close();
3110 if ($lastenrollid and $users) {
3111 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3114 $trace->output('...notification processing finished.');
3115 $trace->finished();
3117 $this->set_config('expirynotifylast', $timenow);
3121 * Returns the user who is responsible for enrolments for given instance.
3123 * Override if plugin knows anybody better than admin.
3125 * @param int $instanceid enrolment instance id
3126 * @return stdClass user record
3128 protected function get_enroller($instanceid) {
3129 return get_admin();
3133 * Notify user about incoming expiration of their enrolment,
3134 * it is called only if notification of enrolled users (aka students) is enabled in course.
3136 * This is executed only once for each expiring enrolment right
3137 * at the start of the expiration threshold.
3139 * @param stdClass $user
3140 * @param stdClass $ue
3141 * @param progress_trace $trace
3143 protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3144 global $CFG;
3146 $name = $this->get_name();
3148 $oldforcelang = force_current_language($user->lang);
3150 $enroller = $this->get_enroller($ue->enrolid);
3151 $context = context_course::instance($ue->courseid);
3153 $a = new stdClass();
3154 $a->course = format_string($ue->fullname, true, array('context'=>$context));
3155 $a->user = fullname($user, true);
3156 $a->timeend = userdate($ue->timeend, '', $user->timezone);
3157 $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3159 $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
3160 $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3162 $message = new \core\message\message();
3163 $message->courseid = $ue->courseid;
3164 $message->notification = 1;
3165 $message->component = 'enrol_'.$name;
3166 $message->name = 'expiry_notification';
3167 $message->userfrom = $enroller;
3168 $message->userto = $user;
3169 $message->subject = $subject;
3170 $message->fullmessage = $body;
3171 $message->fullmessageformat = FORMAT_MARKDOWN;
3172 $message->fullmessagehtml = markdown_to_html($body);
3173 $message->smallmessage = $subject;
3174 $message->contexturlname = $a->course;
3175 $message->contexturl = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
3177 if (message_send($message)) {
3178 $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3179 } else {
3180 $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3183 force_current_language($oldforcelang);
3187 * Notify person responsible for enrolments that some user enrolments will be expired soon,
3188 * it is called only if notification of enrollers (aka teachers) is enabled in course.
3190 * This is called repeatedly every day for each course if there are any pending expiration
3191 * in the expiration threshold.
3193 * @param int $eid
3194 * @param array $users
3195 * @param progress_trace $trace
3197 protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3198 global $DB;
3200 $name = $this->get_name();
3202 $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3203 $context = context_course::instance($instance->courseid);
3204 $course = $DB->get_record('course', array('id'=>$instance->courseid));
3206 $enroller = $this->get_enroller($instance->id);
3207 $admin = get_admin();
3209 $oldforcelang = force_current_language($enroller->lang);
3211 foreach($users as $key=>$info) {
3212 $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3215 $a = new stdClass();
3216 $a->course = format_string($course->fullname, true, array('context'=>$context));
3217 $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3218 $a->users = implode("\n", $users);
3219 $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3221 $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3222 $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3224 $message = new \core\message\message();
3225 $message->courseid = $course->id;
3226 $message->notification = 1;
3227 $message->component = 'enrol_'.$name;
3228 $message->name = 'expiry_notification';
3229 $message->userfrom = $admin;
3230 $message->userto = $enroller;
3231 $message->subject = $subject;
3232 $message->fullmessage = $body;
3233 $message->fullmessageformat = FORMAT_MARKDOWN;
3234 $message->fullmessagehtml = markdown_to_html($body);
3235 $message->smallmessage = $subject;
3236 $message->contexturlname = $a->course;
3237 $message->contexturl = $a->extendurl;
3239 if (message_send($message)) {
3240 $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3241 } else {
3242 $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3245 force_current_language($oldforcelang);
3249 * Backup execution step hook to annotate custom fields.
3251 * @param backup_enrolments_execution_step $step
3252 * @param stdClass $enrol
3254 public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3255 // Override as necessary to annotate custom fields in the enrol table.
3259 * Automatic enrol sync executed during restore.
3260 * Useful for automatic sync by course->idnumber or course category.
3261 * @param stdClass $course course record
3263 public function restore_sync_course($course) {
3264 // Override if necessary.
3268 * Restore instance and map settings.
3270 * @param restore_enrolments_structure_step $step
3271 * @param stdClass $data
3272 * @param stdClass $course
3273 * @param int $oldid
3275 public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3276 // Do not call this from overridden methods, restore and set new id there.
3277 $step->set_mapping('enrol', $oldid, 0);
3281 * Restore user enrolment.
3283 * @param restore_enrolments_structure_step $step
3284 * @param stdClass $data
3285 * @param stdClass $instance
3286 * @param int $oldinstancestatus
3287 * @param int $userid
3289 public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3290 // Override as necessary if plugin supports restore of enrolments.
3294 * Restore role assignment.
3296 * @param stdClass $instance
3297 * @param int $roleid
3298 * @param int $userid
3299 * @param int $contextid
3301 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3302 // No role assignment by default, override if necessary.
3306 * Restore user group membership.
3307 * @param stdClass $instance
3308 * @param int $groupid
3309 * @param int $userid
3311 public function restore_group_member($instance, $groupid, $userid) {
3312 // Implement if you want to restore protected group memberships,
3313 // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3317 * Returns defaults for new instances.
3318 * @since Moodle 3.1
3319 * @return array
3321 public function get_instance_defaults() {
3322 return array();
3326 * Validate a list of parameter names and types.
3327 * @since Moodle 3.1
3329 * @param array $data array of ("fieldname"=>value) of submitted data
3330 * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3331 * @return array of "element_name"=>"error_description" if there are errors,
3332 * or an empty array if everything is OK.
3334 public function validate_param_types($data, $rules) {
3335 $errors = array();
3336 $invalidstr = get_string('invaliddata', 'error');
3337 foreach ($rules as $fieldname => $rule) {
3338 if (is_array($rule)) {
3339 if (!in_array($data[$fieldname], $rule)) {
3340 $errors[$fieldname] = $invalidstr;
3342 } else {
3343 if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3344 $errors[$fieldname] = $invalidstr;
3348 return $errors;