Moodle release 3.11.11
[moodle.git] / lib / enrollib.php
blobc053a3faaab8f90afbfdbaebfbcec4ba0f6aeb94
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
218 * This function may be very slow, use only once after log-in or login-as.
220 * @param stdClass $user
221 * @return void
223 function enrol_check_plugins($user) {
224 global $CFG;
226 if (empty($user->id) or isguestuser($user)) {
227 // shortcut - there is no enrolment work for guests and not-logged-in users
228 return;
231 // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
232 // which proved it was actually not necessary.
234 static $inprogress = array(); // To prevent this function being called more than once in an invocation
236 if (!empty($inprogress[$user->id])) {
237 return;
240 $inprogress[$user->id] = true; // Set the flag
242 $enabled = enrol_get_plugins(true);
244 foreach($enabled as $enrol) {
245 $enrol->sync_user_enrolments($user);
248 unset($inprogress[$user->id]); // Unset the flag
252 * Do these two students share any course?
254 * The courses has to be visible and enrolments has to be active,
255 * timestart and timeend restrictions are ignored.
257 * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
258 * to true.
260 * @param stdClass|int $user1
261 * @param stdClass|int $user2
262 * @return bool
264 function enrol_sharing_course($user1, $user2) {
265 return enrol_get_shared_courses($user1, $user2, false, true);
269 * Returns any courses shared by the two users
271 * The courses has to be visible and enrolments has to be active,
272 * timestart and timeend restrictions are ignored.
274 * @global moodle_database $DB
275 * @param stdClass|int $user1
276 * @param stdClass|int $user2
277 * @param bool $preloadcontexts If set to true contexts for the returned courses
278 * will be preloaded.
279 * @param bool $checkexistsonly If set to true then this function will return true
280 * if the users share any courses and false if not.
281 * @return array|bool An array of courses that both users are enrolled in OR if
282 * $checkexistsonly set returns true if the users share any courses
283 * and false if not.
285 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
286 global $DB, $CFG;
288 $user1 = isset($user1->id) ? $user1->id : $user1;
289 $user2 = isset($user2->id) ? $user2->id : $user2;
291 if (empty($user1) or empty($user2)) {
292 return false;
295 if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
296 return false;
299 list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
300 list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
301 $params = array_merge($params1, $params2);
302 $params['enabled1'] = ENROL_INSTANCE_ENABLED;
303 $params['enabled2'] = ENROL_INSTANCE_ENABLED;
304 $params['active1'] = ENROL_USER_ACTIVE;
305 $params['active2'] = ENROL_USER_ACTIVE;
306 $params['user1'] = $user1;
307 $params['user2'] = $user2;
309 $ctxselect = '';
310 $ctxjoin = '';
311 if ($preloadcontexts) {
312 $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
313 $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
314 $params['contextlevel'] = CONTEXT_COURSE;
317 $sql = "SELECT c.* $ctxselect
318 FROM {course} c
319 JOIN (
320 SELECT DISTINCT c.id
321 FROM {course} c
322 JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
323 JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
324 JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
325 JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
326 WHERE c.visible = 1
327 ) ec ON ec.id = c.id
328 $ctxjoin";
330 if ($checkexistsonly) {
331 return $DB->record_exists_sql($sql, $params);
332 } else {
333 $courses = $DB->get_records_sql($sql, $params);
334 if ($preloadcontexts) {
335 array_map('context_helper::preload_from_record', $courses);
337 return $courses;
342 * This function adds necessary enrol plugins UI into the course edit form.
344 * @param MoodleQuickForm $mform
345 * @param object $data course edit form data
346 * @param object $context context of existing course or parent category if course does not exist
347 * @return void
349 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
350 $plugins = enrol_get_plugins(true);
351 if (!empty($data->id)) {
352 $instances = enrol_get_instances($data->id, false);
353 foreach ($instances as $instance) {
354 if (!isset($plugins[$instance->enrol])) {
355 continue;
357 $plugin = $plugins[$instance->enrol];
358 $plugin->course_edit_form($instance, $mform, $data, $context);
360 } else {
361 foreach ($plugins as $plugin) {
362 $plugin->course_edit_form(NULL, $mform, $data, $context);
368 * Validate course edit form data
370 * @param array $data raw form data
371 * @param object $context context of existing course or parent category if course does not exist
372 * @return array errors array
374 function enrol_course_edit_validation(array $data, $context) {
375 $errors = array();
376 $plugins = enrol_get_plugins(true);
378 if (!empty($data['id'])) {
379 $instances = enrol_get_instances($data['id'], false);
380 foreach ($instances as $instance) {
381 if (!isset($plugins[$instance->enrol])) {
382 continue;
384 $plugin = $plugins[$instance->enrol];
385 $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
387 } else {
388 foreach ($plugins as $plugin) {
389 $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
393 return $errors;
397 * Update enrol instances after course edit form submission
398 * @param bool $inserted true means new course added, false course already existed
399 * @param object $course
400 * @param object $data form data
401 * @return void
403 function enrol_course_updated($inserted, $course, $data) {
404 global $DB, $CFG;
406 $plugins = enrol_get_plugins(true);
408 foreach ($plugins as $plugin) {
409 $plugin->course_updated($inserted, $course, $data);
414 * Add navigation nodes
415 * @param navigation_node $coursenode
416 * @param object $course
417 * @return void
419 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
420 global $CFG;
422 $coursecontext = context_course::instance($course->id);
424 $instances = enrol_get_instances($course->id, true);
425 $plugins = enrol_get_plugins(true);
427 // we do not want to break all course pages if there is some borked enrol plugin, right?
428 foreach ($instances as $k=>$instance) {
429 if (!isset($plugins[$instance->enrol])) {
430 unset($instances[$k]);
434 $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
436 if ($course->id != SITEID) {
437 // list all participants - allows assigning roles, groups, etc.
438 if (has_capability('moodle/course:enrolreview', $coursecontext)) {
439 $url = new moodle_url('/user/index.php', array('id'=>$course->id));
440 $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
443 // manage enrol plugin instances
444 if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
445 $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
446 } else {
447 $url = NULL;
449 $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
451 // each instance decides how to configure itself or how many other nav items are exposed
452 foreach ($instances as $instance) {
453 if (!isset($plugins[$instance->enrol])) {
454 continue;
456 $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
459 if (!$url) {
460 $instancesnode->trim_if_empty();
464 // Manage groups in this course or even frontpage
465 if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
466 $url = new moodle_url('/group/index.php', array('id'=>$course->id));
467 $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
470 if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
471 // Override roles
472 if (has_capability('moodle/role:review', $coursecontext)) {
473 $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
474 } else {
475 $url = NULL;
477 $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
479 // Add assign or override roles if allowed
480 if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
481 if (has_capability('moodle/role:assign', $coursecontext)) {
482 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
483 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
486 // Check role permissions
487 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
488 $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
489 $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
493 // Deal somehow with users that are not enrolled but still got a role somehow
494 if ($course->id != SITEID) {
495 //TODO, create some new UI for role assignments at course level
496 if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
497 $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
498 $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
502 // just in case nothing was actually added
503 $usersnode->trim_if_empty();
505 if ($course->id != SITEID) {
506 if (isguestuser() or !isloggedin()) {
507 // guest account can not be enrolled - no links for them
508 } else if (is_enrolled($coursecontext)) {
509 // unenrol link if possible
510 foreach ($instances as $instance) {
511 if (!isset($plugins[$instance->enrol])) {
512 continue;
514 $plugin = $plugins[$instance->enrol];
515 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
516 $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
517 $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
518 break;
519 //TODO. deal with multiple unenrol links - not likely case, but still...
522 } else {
523 // enrol link if possible
524 if (is_viewing($coursecontext)) {
525 // better not show any enrol link, this is intended for managers and inspectors
526 } else {
527 foreach ($instances as $instance) {
528 if (!isset($plugins[$instance->enrol])) {
529 continue;
531 $plugin = $plugins[$instance->enrol];
532 if ($plugin->show_enrolme_link($instance)) {
533 $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
534 $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
535 $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
536 break;
545 * Returns list of courses current $USER is enrolled in and can access
547 * The $fields param is a list of field names to ADD so name just the fields you really need,
548 * which will be added and uniq'd.
550 * If $allaccessible is true, this will additionally return courses that the current user is not
551 * enrolled in, but can access because they are open to the user for other reasons (course view
552 * permission, currently viewing course as a guest, or course allows guest access without
553 * password).
555 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
556 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
557 * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
558 * "ue" for the user_enrolments table.
559 * @param int $limit max number of courses
560 * @param array $courseids the list of course ids to filter by
561 * @param bool $allaccessible Include courses user is not enrolled in, but can access
562 * @param int $offset Offset the result set by this number
563 * @param array $excludecourses IDs of hidden courses to exclude from search
564 * @return array
566 function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
567 $offset = 0, $excludecourses = []) {
568 global $DB, $USER, $CFG;
570 // Allowed prefixes and field names.
571 $allowedprefixesandfields = ['c' => array_keys($DB->get_columns('course')),
572 'ul' => array_keys($DB->get_columns('user_lastaccess')),
573 'ue' => array_keys($DB->get_columns('user_enrolments'))];
575 // Re-Arrange the course sorting according to the admin settings.
576 $sort = enrol_get_courses_sortingsql($sort);
578 // Guest account does not have any enrolled courses.
579 if (!$allaccessible && (isguestuser() or !isloggedin())) {
580 return array();
583 $basefields = [
584 'id', 'category', 'sortorder',
585 'shortname', 'fullname', 'idnumber',
586 'startdate', 'visible',
587 'groupmode', 'groupmodeforce', 'cacherev',
588 'showactivitydates', 'showcompletionconditions',
591 if (empty($fields)) {
592 $fields = $basefields;
593 } else if (is_string($fields)) {
594 // turn the fields from a string to an array
595 $fields = explode(',', $fields);
596 $fields = array_map('trim', $fields);
597 $fields = array_unique(array_merge($basefields, $fields));
598 } else if (is_array($fields)) {
599 $fields = array_unique(array_merge($basefields, $fields));
600 } else {
601 throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
603 if (in_array('*', $fields)) {
604 $fields = array('*');
607 $orderby = "";
608 $sort = trim($sort);
609 $sorttimeaccess = false;
610 if (!empty($sort)) {
611 $rawsorts = explode(',', $sort);
612 $sorts = array();
613 foreach ($rawsorts as $rawsort) {
614 $rawsort = trim($rawsort);
615 // Make sure that there are no more white spaces in sortparams after explode.
616 $sortparams = array_values(array_filter(explode(' ', $rawsort)));
617 // If more than 2 values present then throw coding_exception.
618 if (isset($sortparams[2])) {
619 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
621 // Check the sort ordering if present, at the beginning.
622 if (isset($sortparams[1]) && (preg_match("/^(asc|desc)$/i", $sortparams[1]) === 0)) {
623 throw new coding_exception('Invalid sort direction in $sort parameter in enrol_get_my_courses()');
626 $sortfield = $sortparams[0];
627 $sortdirection = $sortparams[1] ?? 'asc';
628 if (strpos($sortfield, '.') !== false) {
629 $sortfieldparams = explode('.', $sortfield);
630 // Check if more than one dots present in the prefix field.
631 if (isset($sortfieldparams[2])) {
632 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
634 list($prefix, $fieldname) = [$sortfieldparams[0], $sortfieldparams[1]];
635 // Check if the field name matches with the allowed prefix.
636 if (array_key_exists($prefix, $allowedprefixesandfields) &&
637 (in_array($fieldname, $allowedprefixesandfields[$prefix]))) {
638 if ($prefix === 'ul') {
639 $sorts[] = "COALESCE({$prefix}.{$fieldname}, 0) {$sortdirection}";
640 $sorttimeaccess = true;
641 } else {
642 // Check if the field name that matches with the prefix and just append to sorts.
643 $sorts[] = $rawsort;
645 } else {
646 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
648 } else {
649 // Check if the field name matches with $allowedprefixesandfields.
650 $found = false;
651 foreach (array_keys($allowedprefixesandfields) as $prefix) {
652 if (in_array($sortfield, $allowedprefixesandfields[$prefix])) {
653 if ($prefix === 'ul') {
654 $sorts[] = "COALESCE({$prefix}.{$sortfield}, 0) {$sortdirection}";
655 $sorttimeaccess = true;
656 } else {
657 $sorts[] = "{$prefix}.{$sortfield} {$sortdirection}";
659 $found = true;
660 break;
663 if (!$found) {
664 // The param is not found in $allowedprefixesandfields.
665 throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
669 $sort = implode(',', $sorts);
670 $orderby = "ORDER BY $sort";
673 $wheres = ['c.id <> ' . SITEID];
674 $params = [];
676 if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
677 // list _only_ this course - anything else is asking for trouble...
678 $wheres[] = "courseid = :loginas";
679 $params['loginas'] = $USER->loginascontext->instanceid;
682 $coursefields = 'c.' .join(',c.', $fields);
683 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
684 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
685 $params['contextlevel'] = CONTEXT_COURSE;
686 $wheres = implode(" AND ", $wheres);
688 $timeaccessselect = "";
689 $timeaccessjoin = "";
691 if (!empty($courseids)) {
692 list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
693 $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
694 $params = array_merge($params, $courseidsparams);
697 if (!empty($excludecourses)) {
698 list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
699 $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
700 $params = array_merge($params, $courseidsparams);
703 $courseidsql = "";
704 // Logged-in, non-guest users get their enrolled courses.
705 if (!isguestuser() && isloggedin()) {
706 $courseidsql .= "
707 SELECT DISTINCT e.courseid
708 FROM {enrol} e
709 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
710 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart <= :now1
711 AND (ue.timeend = 0 OR ue.timeend > :now2)";
712 $params['userid1'] = $USER->id;
713 $params['active'] = ENROL_USER_ACTIVE;
714 $params['enabled'] = ENROL_INSTANCE_ENABLED;
715 $params['now1'] = $params['now2'] = time();
717 if ($sorttimeaccess) {
718 $params['userid2'] = $USER->id;
719 $timeaccessselect = ', ul.timeaccess as lastaccessed';
720 $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
724 // When including non-enrolled but accessible courses...
725 if ($allaccessible) {
726 if (is_siteadmin()) {
727 // Site admins can access all courses.
728 $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
729 } else {
730 // If we used the enrolment as well, then this will be UNIONed.
731 if ($courseidsql) {
732 $courseidsql .= " UNION ";
735 // Include courses with guest access and no password.
736 $courseidsql .= "
737 SELECT DISTINCT e.courseid
738 FROM {enrol} e
739 WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
740 $params['emptypass'] = '';
741 $params['enabled2'] = ENROL_INSTANCE_ENABLED;
743 // Include courses where the current user is currently using guest access (may include
744 // those which require a password).
745 $courseids = [];
746 $accessdata = get_user_accessdata($USER->id);
747 foreach ($accessdata['ra'] as $contextpath => $roles) {
748 if (array_key_exists($CFG->guestroleid, $roles)) {
749 // Work out the course id from context path.
750 $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
751 if ($context instanceof context_course) {
752 $courseids[$context->instanceid] = true;
757 // Include courses where the current user has moodle/course:view capability.
758 $courses = get_user_capability_course('moodle/course:view', null, false);
759 if (!$courses) {
760 $courses = [];
762 foreach ($courses as $course) {
763 $courseids[$course->id] = true;
766 // If there are any in either category, list them individually.
767 if ($courseids) {
768 list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
769 array_keys($courseids), SQL_PARAMS_NAMED);
770 $courseidsql .= "
771 UNION
772 SELECT DISTINCT c3.id AS courseid
773 FROM {course} c3
774 WHERE c3.id $allowedsql";
775 $params = array_merge($params, $allowedparams);
780 // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
781 // we have the subselect there.
782 $sql = "SELECT $coursefields $ccselect $timeaccessselect
783 FROM {course} c
784 JOIN ($courseidsql) en ON (en.courseid = c.id)
785 $timeaccessjoin
786 $ccjoin
787 WHERE $wheres
788 $orderby";
790 $courses = $DB->get_records_sql($sql, $params, $offset, $limit);
792 // preload contexts and check visibility
793 foreach ($courses as $id=>$course) {
794 context_helper::preload_from_record($course);
795 if (!$course->visible) {
796 if (!$context = context_course::instance($id, IGNORE_MISSING)) {
797 unset($courses[$id]);
798 continue;
800 if (!has_capability('moodle/course:viewhiddencourses', $context)) {
801 unset($courses[$id]);
802 continue;
805 $courses[$id] = $course;
808 //wow! Is that really all? :-D
810 return $courses;
814 * Returns course enrolment information icons.
816 * @param object $course
817 * @param array $instances enrol instances of this course, improves performance
818 * @return array of pix_icon
820 function enrol_get_course_info_icons($course, array $instances = NULL) {
821 $icons = array();
822 if (is_null($instances)) {
823 $instances = enrol_get_instances($course->id, true);
825 $plugins = enrol_get_plugins(true);
826 foreach ($plugins as $name => $plugin) {
827 $pis = array();
828 foreach ($instances as $instance) {
829 if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
830 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
831 continue;
833 if ($instance->enrol == $name) {
834 $pis[$instance->id] = $instance;
837 if ($pis) {
838 $icons = array_merge($icons, $plugin->get_info_icons($pis));
841 return $icons;
845 * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
847 * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
848 * @return string SQL ORDER arguments.
850 function enrol_get_courses_sortingsql($sort = null) {
851 global $CFG;
853 // Prepare the visible SQL fragment as empty.
854 $visible = '';
855 // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
856 if ($sort === null || strpos($sort, 'visible') === false) {
857 // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
858 // courses to the end of the course list.
859 if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
860 $visible = 'visible DESC, ';
864 // Only create a sortorder SQL fragment if the caller didn't already pass one.
865 if ($sort === null) {
866 // If the admin has configured a course sort order, we will use this.
867 if (!empty($CFG->navsortmycoursessort)) {
868 $sort = $CFG->navsortmycoursessort . ' ASC';
870 // Otherwise we will fall back to the sortorder sorting.
871 } else {
872 $sort = 'sortorder ASC';
876 return $visible . $sort;
880 * Returns course enrolment detailed information.
882 * @param object $course
883 * @return array of html fragments - can be used to construct lists
885 function enrol_get_course_description_texts($course) {
886 $lines = array();
887 $instances = enrol_get_instances($course->id, true);
888 $plugins = enrol_get_plugins(true);
889 foreach ($instances as $instance) {
890 if (!isset($plugins[$instance->enrol])) {
891 //weird
892 continue;
894 $plugin = $plugins[$instance->enrol];
895 $text = $plugin->get_description_text($instance);
896 if ($text !== NULL) {
897 $lines[] = $text;
900 return $lines;
904 * Returns list of courses user is enrolled into.
906 * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
908 * The $fields param is a list of field names to ADD so name just the fields you really need,
909 * which will be added and uniq'd.
911 * @param int $userid User whose courses are returned, defaults to the current user.
912 * @param bool $onlyactive Return only active enrolments in courses user may see.
913 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
914 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
915 * @return array
917 function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
918 global $DB;
920 $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
922 // preload contexts and check visibility
923 if ($onlyactive) {
924 foreach ($courses as $id=>$course) {
925 context_helper::preload_from_record($course);
926 if (!$course->visible) {
927 if (!$context = context_course::instance($id)) {
928 unset($courses[$id]);
929 continue;
931 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
932 unset($courses[$id]);
933 continue;
939 return $courses;
943 * Returns list of roles per users into course.
945 * @param int $courseid Course id.
946 * @return array Array[$userid][$roleid] = role_assignment.
948 function enrol_get_course_users_roles(int $courseid) : array {
949 global $DB;
951 $context = context_course::instance($courseid);
953 $roles = array();
955 $records = $DB->get_recordset('role_assignments', array('contextid' => $context->id));
956 foreach ($records as $record) {
957 if (isset($roles[$record->userid]) === false) {
958 $roles[$record->userid] = array();
960 $roles[$record->userid][$record->roleid] = $record;
962 $records->close();
964 return $roles;
968 * Can user access at least one enrolled course?
970 * Cheat if necessary, but find out as fast as possible!
972 * @param int|stdClass $user null means use current user
973 * @return bool
975 function enrol_user_sees_own_courses($user = null) {
976 global $USER;
978 if ($user === null) {
979 $user = $USER;
981 $userid = is_object($user) ? $user->id : $user;
983 // Guest account does not have any courses
984 if (isguestuser($userid) or empty($userid)) {
985 return false;
988 // Let's cheat here if this is the current user,
989 // if user accessed any course recently, then most probably
990 // we do not need to query the database at all.
991 if ($USER->id == $userid) {
992 if (!empty($USER->enrol['enrolled'])) {
993 foreach ($USER->enrol['enrolled'] as $until) {
994 if ($until > time()) {
995 return true;
1001 // Now the slow way.
1002 $courses = enrol_get_all_users_courses($userid, true);
1003 foreach($courses as $course) {
1004 if ($course->visible) {
1005 return true;
1007 context_helper::preload_from_record($course);
1008 $context = context_course::instance($course->id);
1009 if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
1010 return true;
1014 return false;
1018 * Returns list of courses user is enrolled into without performing any capability checks.
1020 * The $fields param is a list of field names to ADD so name just the fields you really need,
1021 * which will be added and uniq'd.
1023 * @param int $userid User whose courses are returned, defaults to the current user.
1024 * @param bool $onlyactive Return only active enrolments in courses user may see.
1025 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
1026 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
1027 * @return array
1029 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
1030 global $DB;
1032 // Re-Arrange the course sorting according to the admin settings.
1033 $sort = enrol_get_courses_sortingsql($sort);
1035 // Guest account does not have any courses
1036 if (isguestuser($userid) or empty($userid)) {
1037 return(array());
1040 $basefields = array('id', 'category', 'sortorder',
1041 'shortname', 'fullname', 'idnumber',
1042 'startdate', 'visible',
1043 'defaultgroupingid',
1044 'groupmode', 'groupmodeforce');
1046 if (empty($fields)) {
1047 $fields = $basefields;
1048 } else if (is_string($fields)) {
1049 // turn the fields from a string to an array
1050 $fields = explode(',', $fields);
1051 $fields = array_map('trim', $fields);
1052 $fields = array_unique(array_merge($basefields, $fields));
1053 } else if (is_array($fields)) {
1054 $fields = array_unique(array_merge($basefields, $fields));
1055 } else {
1056 throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
1058 if (in_array('*', $fields)) {
1059 $fields = array('*');
1062 $orderby = "";
1063 $sort = trim($sort);
1064 if (!empty($sort)) {
1065 $rawsorts = explode(',', $sort);
1066 $sorts = array();
1067 foreach ($rawsorts as $rawsort) {
1068 $rawsort = trim($rawsort);
1069 if (strpos($rawsort, 'c.') === 0) {
1070 $rawsort = substr($rawsort, 2);
1072 $sorts[] = trim($rawsort);
1074 $sort = 'c.'.implode(',c.', $sorts);
1075 $orderby = "ORDER BY $sort";
1078 $params = [];
1080 if ($onlyactive) {
1081 $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
1082 $params['now1'] = round(time(), -2); // improves db caching
1083 $params['now2'] = $params['now1'];
1084 $params['active'] = ENROL_USER_ACTIVE;
1085 $params['enabled'] = ENROL_INSTANCE_ENABLED;
1086 } else {
1087 $subwhere = "";
1090 $coursefields = 'c.' .join(',c.', $fields);
1091 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1092 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1093 $params['contextlevel'] = CONTEXT_COURSE;
1095 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
1096 $sql = "SELECT $coursefields $ccselect
1097 FROM {course} c
1098 JOIN (SELECT DISTINCT e.courseid
1099 FROM {enrol} e
1100 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1101 $subwhere
1102 ) en ON (en.courseid = c.id)
1103 $ccjoin
1104 WHERE c.id <> " . SITEID . "
1105 $orderby";
1106 $params['userid'] = $userid;
1108 $courses = $DB->get_records_sql($sql, $params);
1110 return $courses;
1116 * Called when user is about to be deleted.
1117 * @param object $user
1118 * @return void
1120 function enrol_user_delete($user) {
1121 global $DB;
1123 $plugins = enrol_get_plugins(true);
1124 foreach ($plugins as $plugin) {
1125 $plugin->user_delete($user);
1128 // force cleanup of all broken enrolments
1129 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1133 * Called when course is about to be deleted.
1134 * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
1135 * otherwise all enrolments in the course will be removed.
1137 * @param stdClass $course
1138 * @param int|null $userid
1139 * @return void
1141 function enrol_course_delete($course, $userid = null) {
1142 global $DB;
1144 $context = context_course::instance($course->id);
1145 $instances = enrol_get_instances($course->id, false);
1146 $plugins = enrol_get_plugins(true);
1148 if ($userid) {
1149 // If the user id is present, include only course enrolment instances which allow manual unenrolment and
1150 // the given user have a capability to perform unenrolment.
1151 $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
1152 $unenrolcap = "enrol/{$instance->enrol}:unenrol";
1153 return $plugins[$instance->enrol]->allow_unenrol($instance) &&
1154 has_capability($unenrolcap, $context, $userid);
1158 foreach ($instances as $instance) {
1159 if (isset($plugins[$instance->enrol])) {
1160 $plugins[$instance->enrol]->delete_instance($instance);
1162 // low level delete in case plugin did not do it
1163 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1164 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1165 $DB->delete_records('enrol', array('id'=>$instance->id));
1170 * Try to enrol user via default internal auth plugin.
1172 * For now this is always using the manual enrol plugin...
1174 * @param $courseid
1175 * @param $userid
1176 * @param $roleid
1177 * @param $timestart
1178 * @param $timeend
1179 * @return bool success
1181 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1182 global $DB;
1184 //note: this is hardcoded to manual plugin for now
1186 if (!enrol_is_enabled('manual')) {
1187 return false;
1190 if (!$enrol = enrol_get_plugin('manual')) {
1191 return false;
1193 if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1194 return false;
1196 $instance = reset($instances);
1198 $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1200 return true;
1204 * Is there a chance users might self enrol
1205 * @param int $courseid
1206 * @return bool
1208 function enrol_selfenrol_available($courseid) {
1209 $result = false;
1211 $plugins = enrol_get_plugins(true);
1212 $enrolinstances = enrol_get_instances($courseid, true);
1213 foreach($enrolinstances as $instance) {
1214 if (!isset($plugins[$instance->enrol])) {
1215 continue;
1217 if ($instance->enrol === 'guest') {
1218 continue;
1220 if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
1221 $result = true;
1222 break;
1226 return $result;
1230 * This function returns the end of current active user enrolment.
1232 * It deals correctly with multiple overlapping user enrolments.
1234 * @param int $courseid
1235 * @param int $userid
1236 * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1238 function enrol_get_enrolment_end($courseid, $userid) {
1239 global $DB;
1241 $sql = "SELECT ue.*
1242 FROM {user_enrolments} ue
1243 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1244 JOIN {user} u ON u.id = ue.userid
1245 WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1246 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1248 if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1249 return false;
1252 $changes = array();
1254 foreach ($enrolments as $ue) {
1255 $start = (int)$ue->timestart;
1256 $end = (int)$ue->timeend;
1257 if ($end != 0 and $end < $start) {
1258 debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1259 continue;
1261 if (isset($changes[$start])) {
1262 $changes[$start] = $changes[$start] + 1;
1263 } else {
1264 $changes[$start] = 1;
1266 if ($end === 0) {
1267 // no end
1268 } else if (isset($changes[$end])) {
1269 $changes[$end] = $changes[$end] - 1;
1270 } else {
1271 $changes[$end] = -1;
1275 // let's sort then enrolment starts&ends and go through them chronologically,
1276 // looking for current status and the next future end of enrolment
1277 ksort($changes);
1279 $now = time();
1280 $current = 0;
1281 $present = null;
1283 foreach ($changes as $time => $change) {
1284 if ($time > $now) {
1285 if ($present === null) {
1286 // we have just went past current time
1287 $present = $current;
1288 if ($present < 1) {
1289 // no enrolment active
1290 return false;
1293 if ($present !== null) {
1294 // we are already in the future - look for possible end
1295 if ($current + $change < 1) {
1296 return $time;
1300 $current += $change;
1303 if ($current > 0) {
1304 return 0;
1305 } else {
1306 return false;
1311 * Is current user accessing course via this enrolment method?
1313 * This is intended for operations that are going to affect enrol instances.
1315 * @param stdClass $instance enrol instance
1316 * @return bool
1318 function enrol_accessing_via_instance(stdClass $instance) {
1319 global $DB, $USER;
1321 if (empty($instance->id)) {
1322 return false;
1325 if (is_siteadmin()) {
1326 // Admins may go anywhere.
1327 return false;
1330 return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1334 * Returns true if user is enrolled (is participating) in course
1335 * this is intended for students and teachers.
1337 * Since 2.2 the result for active enrolments and current user are cached.
1339 * @param context $context
1340 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1341 * @param string $withcapability extra capability name
1342 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1343 * @return bool
1345 function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1346 global $USER, $DB;
1348 // First find the course context.
1349 $coursecontext = $context->get_course_context();
1351 // Make sure there is a real user specified.
1352 if ($user === null) {
1353 $userid = isset($USER->id) ? $USER->id : 0;
1354 } else {
1355 $userid = is_object($user) ? $user->id : $user;
1358 if (empty($userid)) {
1359 // Not-logged-in!
1360 return false;
1361 } else if (isguestuser($userid)) {
1362 // Guest account can not be enrolled anywhere.
1363 return false;
1366 // Note everybody participates on frontpage, so for other contexts...
1367 if ($coursecontext->instanceid != SITEID) {
1368 // Try cached info first - the enrolled flag is set only when active enrolment present.
1369 if ($USER->id == $userid) {
1370 $coursecontext->reload_if_dirty();
1371 if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1372 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1373 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1374 return false;
1376 return true;
1381 if ($onlyactive) {
1382 // Look for active enrolments only.
1383 $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1385 if ($until === false) {
1386 return false;
1389 if ($USER->id == $userid) {
1390 if ($until == 0) {
1391 $until = ENROL_MAX_TIMESTAMP;
1393 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1394 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1395 unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1396 remove_temp_course_roles($coursecontext);
1400 } else {
1401 // Any enrolment is good for us here, even outdated, disabled or inactive.
1402 $sql = "SELECT 'x'
1403 FROM {user_enrolments} ue
1404 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1405 JOIN {user} u ON u.id = ue.userid
1406 WHERE ue.userid = :userid AND u.deleted = 0";
1407 $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1408 if (!$DB->record_exists_sql($sql, $params)) {
1409 return false;
1414 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1415 return false;
1418 return true;
1422 * Returns an array of joins, wheres and params that will limit the group of
1423 * users to only those enrolled and with given capability (if specified).
1425 * Note this join will return duplicate rows for users who have been enrolled
1426 * several times (e.g. as manual enrolment, and as self enrolment). You may
1427 * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1429 * In case is guaranteed some of the joins never match any rows, the resulting
1430 * join_sql->cannotmatchanyrows will be true. This happens when the capability
1431 * is prohibited.
1433 * @param context $context
1434 * @param string $prefix optional, a prefix to the user id column
1435 * @param string|array $capability optional, may include a capability name, or array of names.
1436 * If an array is provided then this is the equivalent of a logical 'OR',
1437 * i.e. the user needs to have one of these capabilities.
1438 * @param int $group optional, 0 indicates no current group and USERSWITHOUTGROUP users without any group; otherwise the group id
1439 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1440 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1441 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1442 * @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
1444 function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
1445 $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1446 $uid = $prefix . 'u.id';
1447 $joins = array();
1448 $wheres = array();
1449 $cannotmatchanyrows = false;
1451 $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1452 $joins[] = $enrolledjoin->joins;
1453 $wheres[] = $enrolledjoin->wheres;
1454 $params = $enrolledjoin->params;
1455 $cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
1457 if (!empty($capability)) {
1458 $capjoin = get_with_capability_join($context, $capability, $uid);
1459 $joins[] = $capjoin->joins;
1460 $wheres[] = $capjoin->wheres;
1461 $params = array_merge($params, $capjoin->params);
1462 $cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
1465 if ($group) {
1466 $groupjoin = groups_get_members_join($group, $uid, $context);
1467 $joins[] = $groupjoin->joins;
1468 $params = array_merge($params, $groupjoin->params);
1469 if (!empty($groupjoin->wheres)) {
1470 $wheres[] = $groupjoin->wheres;
1472 $cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
1475 $joins = implode("\n", $joins);
1476 $wheres[] = "{$prefix}u.deleted = 0";
1477 $wheres = implode(" AND ", $wheres);
1479 return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
1483 * Returns array with sql code and parameters returning all ids
1484 * of users enrolled into course.
1486 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1488 * @param context $context
1489 * @param string $withcapability
1490 * @param int $groupid 0 means ignore groups, USERSWITHOUTGROUP without any group and any other value limits the result by group id
1491 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1492 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1493 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1494 * @return array list($sql, $params)
1496 function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false,
1497 $enrolid = 0) {
1499 // Use unique prefix just in case somebody makes some SQL magic with the result.
1500 static $i = 0;
1501 $i++;
1502 $prefix = 'eu' . $i . '_';
1504 $capjoin = get_enrolled_with_capabilities_join(
1505 $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended, $enrolid);
1507 $sql = "SELECT DISTINCT {$prefix}u.id
1508 FROM {user} {$prefix}u
1509 $capjoin->joins
1510 WHERE $capjoin->wheres";
1512 return array($sql, $capjoin->params);
1516 * Returns array with sql joins and parameters returning all ids
1517 * of users enrolled into course.
1519 * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1521 * @throws coding_exception
1523 * @param context $context
1524 * @param string $useridcolumn User id column used the calling query, e.g. u.id
1525 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1526 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1527 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1528 * @return \core\dml\sql_join Contains joins, wheres, params
1530 function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1531 // Use unique prefix just in case somebody makes some SQL magic with the result.
1532 static $i = 0;
1533 $i++;
1534 $prefix = 'ej' . $i . '_';
1536 // First find the course context.
1537 $coursecontext = $context->get_course_context();
1539 $isfrontpage = ($coursecontext->instanceid == SITEID);
1541 if ($onlyactive && $onlysuspended) {
1542 throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1544 if ($isfrontpage && $onlysuspended) {
1545 throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1548 $joins = array();
1549 $wheres = array();
1550 $params = array();
1552 $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1554 // Note all users are "enrolled" on the frontpage, but for others...
1555 if (!$isfrontpage) {
1556 $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1557 $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1559 $enrolconditions = array(
1560 "{$prefix}e.id = {$prefix}ue.enrolid",
1561 "{$prefix}e.courseid = :{$prefix}courseid",
1563 if ($enrolid) {
1564 $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1565 $params[$prefix . 'enrolid'] = $enrolid;
1567 $enrolconditionssql = implode(" AND ", $enrolconditions);
1568 $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1570 $params[$prefix.'courseid'] = $coursecontext->instanceid;
1572 if (!$onlysuspended) {
1573 $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1574 $joins[] = $ejoin;
1575 if ($onlyactive) {
1576 $wheres[] = "$where1 AND $where2";
1578 } else {
1579 // Suspended only where there is enrolment but ALL are suspended.
1580 // Consider multiple enrols where one is not suspended or plain role_assign.
1581 $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1582 $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1583 $enrolconditions = array(
1584 "{$prefix}e1.id = {$prefix}ue1.enrolid",
1585 "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1587 if ($enrolid) {
1588 $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1589 $params[$prefix . 'e1_enrolid'] = $enrolid;
1591 $enrolconditionssql = implode(" AND ", $enrolconditions);
1592 $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1593 $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1594 $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1597 if ($onlyactive || $onlysuspended) {
1598 $now = round(time(), -2); // Rounding helps caching in DB.
1599 $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1600 $prefix . 'active' => ENROL_USER_ACTIVE,
1601 $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1605 $joins = implode("\n", $joins);
1606 $wheres = implode(" AND ", $wheres);
1608 return new \core\dml\sql_join($joins, $wheres, $params);
1612 * Returns list of users enrolled into course.
1614 * @param context $context
1615 * @param string $withcapability
1616 * @param int $groupid 0 means ignore groups, USERSWITHOUTGROUP without any group and any other value limits the result by group id
1617 * @param string $userfields requested user record fields
1618 * @param string $orderby
1619 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1620 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1621 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1622 * @return array of user records
1624 function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
1625 $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1626 global $DB;
1628 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
1629 $sql = "SELECT $userfields
1630 FROM {user} u
1631 JOIN ($esql) je ON je.id = u.id
1632 WHERE u.deleted = 0";
1634 if ($orderby) {
1635 $sql = "$sql ORDER BY $orderby";
1636 } else {
1637 list($sort, $sortparams) = users_order_by_sql('u');
1638 $sql = "$sql ORDER BY $sort";
1639 $params = array_merge($params, $sortparams);
1642 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1646 * Counts list of users enrolled into course (as per above function)
1648 * @param context $context
1649 * @param string $withcapability
1650 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1651 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1652 * @return int number of users enrolled into course
1654 function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
1655 global $DB;
1657 $capjoin = get_enrolled_with_capabilities_join(
1658 $context, '', $withcapability, $groupid, $onlyactive);
1660 $sql = "SELECT COUNT(DISTINCT u.id)
1661 FROM {user} u
1662 $capjoin->joins
1663 WHERE $capjoin->wheres AND u.deleted = 0";
1665 return $DB->count_records_sql($sql, $capjoin->params);
1669 * Send welcome email "from" options.
1671 * @return array list of from options
1673 function enrol_send_welcome_email_options() {
1674 return [
1675 ENROL_DO_NOT_SEND_EMAIL => get_string('no'),
1676 ENROL_SEND_EMAIL_FROM_COURSE_CONTACT => get_string('sendfromcoursecontact', 'enrol'),
1677 ENROL_SEND_EMAIL_FROM_KEY_HOLDER => get_string('sendfromkeyholder', 'enrol'),
1678 ENROL_SEND_EMAIL_FROM_NOREPLY => get_string('sendfromnoreply', 'enrol')
1683 * Serve the user enrolment form as a fragment.
1685 * @param array $args List of named arguments for the fragment loader.
1686 * @return string
1688 function enrol_output_fragment_user_enrolment_form($args) {
1689 global $CFG, $DB;
1691 $args = (object) $args;
1692 $context = $args->context;
1693 require_capability('moodle/course:enrolreview', $context);
1695 $ueid = $args->ueid;
1696 $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1697 $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1698 $plugin = enrol_get_plugin($instance->enrol);
1699 $customdata = [
1700 'ue' => $userenrolment,
1701 'modal' => true,
1702 'enrolinstancename' => $plugin->get_instance_name($instance)
1705 // Set the data if applicable.
1706 $data = [];
1707 if (isset($args->formdata)) {
1708 $serialiseddata = json_decode($args->formdata);
1709 parse_str($serialiseddata, $data);
1712 require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1713 $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1715 if (!empty($data)) {
1716 $mform->set_data($data);
1717 $mform->is_validated();
1720 return $mform->render();
1724 * Returns the course where a user enrolment belong to.
1726 * @param int $ueid user_enrolments id
1727 * @return stdClass
1729 function enrol_get_course_by_user_enrolment_id($ueid) {
1730 global $DB;
1731 $sql = "SELECT c.* FROM {user_enrolments} ue
1732 JOIN {enrol} e ON e.id = ue.enrolid
1733 JOIN {course} c ON c.id = e.courseid
1734 WHERE ue.id = :ueid";
1735 return $DB->get_record_sql($sql, array('ueid' => $ueid));
1739 * Return all users enrolled in a course.
1741 * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1742 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1743 * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1744 * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1745 * @return stdClass[]
1747 function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
1748 global $DB;
1750 if (!$courseid && !$usersfilter && !$uefilter) {
1751 throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1754 $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1755 ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1756 ue.timemodified AS uetimemodified, e.status AS estatus,
1757 u.* FROM {user_enrolments} ue
1758 JOIN {enrol} e ON e.id = ue.enrolid
1759 JOIN {user} u ON ue.userid = u.id
1760 WHERE ";
1761 $params = array();
1763 if ($courseid) {
1764 $conditions[] = "e.courseid = :courseid";
1765 $params['courseid'] = $courseid;
1768 if ($onlyactive) {
1769 $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1770 "(ue.timeend = 0 OR ue.timeend > :now2)";
1771 // Improves db caching.
1772 $params['now1'] = round(time(), -2);
1773 $params['now2'] = $params['now1'];
1774 $params['active'] = ENROL_USER_ACTIVE;
1775 $params['enabled'] = ENROL_INSTANCE_ENABLED;
1778 if ($usersfilter) {
1779 list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1780 $conditions[] = "ue.userid $usersql";
1781 $params = $params + $userparams;
1784 if ($uefilter) {
1785 list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1786 $conditions[] = "ue.id $uesql";
1787 $params = $params + $ueparams;
1790 return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1794 * Get the list of options for the enrolment period dropdown
1796 * @return array List of options for the enrolment period dropdown
1798 function enrol_get_period_list() {
1799 $periodmenu = [];
1800 $periodmenu[''] = get_string('unlimited');
1801 for ($i = 1; $i <= 365; $i++) {
1802 $seconds = $i * DAYSECS;
1803 $periodmenu[$seconds] = get_string('numdays', '', $i);
1805 return $periodmenu;
1809 * Calculate duration base on start time and end time
1811 * @param int $timestart Time start
1812 * @param int $timeend Time end
1813 * @return float|int Calculated duration
1815 function enrol_calculate_duration($timestart, $timeend) {
1816 $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1817 return $duration;
1821 * Enrolment plugins abstract class.
1823 * All enrol plugins should be based on this class,
1824 * this is also the main source of documentation.
1826 * @copyright 2010 Petr Skoda {@link http://skodak.org}
1827 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1829 abstract class enrol_plugin {
1830 protected $config = null;
1833 * Returns name of this enrol plugin
1834 * @return string
1836 public function get_name() {
1837 // second word in class is always enrol name, sorry, no fancy plugin names with _
1838 $words = explode('_', get_class($this));
1839 return $words[1];
1843 * Returns localised name of enrol instance
1845 * @param object $instance (null is accepted too)
1846 * @return string
1848 public function get_instance_name($instance) {
1849 if (empty($instance->name)) {
1850 $enrol = $this->get_name();
1851 return get_string('pluginname', 'enrol_'.$enrol);
1852 } else {
1853 $context = context_course::instance($instance->courseid);
1854 return format_string($instance->name, true, array('context'=>$context));
1859 * Returns optional enrolment information icons.
1861 * This is used in course list for quick overview of enrolment options.
1863 * We are not using single instance parameter because sometimes
1864 * we might want to prevent icon repetition when multiple instances
1865 * of one type exist. One instance may also produce several icons.
1867 * @param array $instances all enrol instances of this type in one course
1868 * @return array of pix_icon
1870 public function get_info_icons(array $instances) {
1871 return array();
1875 * Returns optional enrolment instance description text.
1877 * This is used in detailed course information.
1880 * @param object $instance
1881 * @return string short html text
1883 public function get_description_text($instance) {
1884 return null;
1888 * Makes sure config is loaded and cached.
1889 * @return void
1891 protected function load_config() {
1892 if (!isset($this->config)) {
1893 $name = $this->get_name();
1894 $this->config = get_config("enrol_$name");
1899 * Returns plugin config value
1900 * @param string $name
1901 * @param string $default value if config does not exist yet
1902 * @return string value or default
1904 public function get_config($name, $default = NULL) {
1905 $this->load_config();
1906 return isset($this->config->$name) ? $this->config->$name : $default;
1910 * Sets plugin config value
1911 * @param string $name name of config
1912 * @param string $value string config value, null means delete
1913 * @return string value
1915 public function set_config($name, $value) {
1916 $pluginname = $this->get_name();
1917 $this->load_config();
1918 if ($value === NULL) {
1919 unset($this->config->$name);
1920 } else {
1921 $this->config->$name = $value;
1923 set_config($name, $value, "enrol_$pluginname");
1927 * Does this plugin assign protected roles are can they be manually removed?
1928 * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1930 public function roles_protected() {
1931 return true;
1935 * Does this plugin allow manual enrolments?
1937 * @param stdClass $instance course enrol instance
1938 * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1940 * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1942 public function allow_enrol(stdClass $instance) {
1943 return false;
1947 * Does this plugin allow manual unenrolment of all users?
1948 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1950 * @param stdClass $instance course enrol instance
1951 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1953 public function allow_unenrol(stdClass $instance) {
1954 return false;
1958 * Does this plugin allow manual unenrolment of a specific user?
1959 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1961 * This is useful especially for synchronisation plugins that
1962 * do suspend instead of full unenrolment.
1964 * @param stdClass $instance course enrol instance
1965 * @param stdClass $ue record from user_enrolments table, specifies user
1967 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1969 public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1970 return $this->allow_unenrol($instance);
1974 * Does this plugin allow manual changes in user_enrolments table?
1976 * All plugins allowing this must implement 'enrol/xxx:manage' capability
1978 * @param stdClass $instance course enrol instance
1979 * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1981 public function allow_manage(stdClass $instance) {
1982 return false;
1986 * Does this plugin support some way to user to self enrol?
1988 * @param stdClass $instance course enrol instance
1990 * @return bool - true means show "Enrol me in this course" link in course UI
1992 public function show_enrolme_link(stdClass $instance) {
1993 return false;
1997 * Attempt to automatically enrol current user in course without any interaction,
1998 * calling code has to make sure the plugin and instance are active.
2000 * This should return either a timestamp in the future or false.
2002 * @param stdClass $instance course enrol instance
2003 * @return bool|int false means not enrolled, integer means timeend
2005 public function try_autoenrol(stdClass $instance) {
2006 global $USER;
2008 return false;
2012 * Attempt to automatically gain temporary guest access to course,
2013 * calling code has to make sure the plugin and instance are active.
2015 * This should return either a timestamp in the future or false.
2017 * @param stdClass $instance course enrol instance
2018 * @return bool|int false means no guest access, integer means timeend
2020 public function try_guestaccess(stdClass $instance) {
2021 global $USER;
2023 return false;
2027 * Enrol user into course via enrol instance.
2029 * @param stdClass $instance
2030 * @param int $userid
2031 * @param int $roleid optional role id
2032 * @param int $timestart 0 means unknown
2033 * @param int $timeend 0 means forever
2034 * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
2035 * @param bool $recovergrades restore grade history
2036 * @return void
2038 public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
2039 global $DB, $USER, $CFG; // CFG necessary!!!
2041 if ($instance->courseid == SITEID) {
2042 throw new coding_exception('invalid attempt to enrol into frontpage course!');
2045 $name = $this->get_name();
2046 $courseid = $instance->courseid;
2048 if ($instance->enrol !== $name) {
2049 throw new coding_exception('invalid enrol instance!');
2051 $context = context_course::instance($instance->courseid, MUST_EXIST);
2052 if (!isset($recovergrades)) {
2053 $recovergrades = $CFG->recovergradesdefault;
2056 $inserted = false;
2057 $updated = false;
2058 if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2059 //only update if timestart or timeend or status are different.
2060 if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
2061 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
2063 } else {
2064 $ue = new stdClass();
2065 $ue->enrolid = $instance->id;
2066 $ue->status = is_null($status) ? ENROL_USER_ACTIVE : $status;
2067 $ue->userid = $userid;
2068 $ue->timestart = $timestart;
2069 $ue->timeend = $timeend;
2070 $ue->modifierid = $USER->id;
2071 $ue->timecreated = time();
2072 $ue->timemodified = $ue->timecreated;
2073 $ue->id = $DB->insert_record('user_enrolments', $ue);
2075 $inserted = true;
2078 if ($inserted) {
2079 // Trigger event.
2080 $event = \core\event\user_enrolment_created::create(
2081 array(
2082 'objectid' => $ue->id,
2083 'courseid' => $courseid,
2084 'context' => $context,
2085 'relateduserid' => $ue->userid,
2086 'other' => array('enrol' => $name)
2089 $event->trigger();
2090 // Check if course contacts cache needs to be cleared.
2091 core_course_category::user_enrolment_changed($courseid, $ue->userid,
2092 $ue->status, $ue->timestart, $ue->timeend);
2095 if ($roleid) {
2096 // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2097 if ($this->roles_protected()) {
2098 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2099 } else {
2100 role_assign($roleid, $userid, $context->id);
2104 // Recover old grades if present.
2105 if ($recovergrades) {
2106 require_once("$CFG->libdir/gradelib.php");
2107 grade_recover_history_grades($userid, $courseid);
2110 // reset current user enrolment caching
2111 if ($userid == $USER->id) {
2112 if (isset($USER->enrol['enrolled'][$courseid])) {
2113 unset($USER->enrol['enrolled'][$courseid]);
2115 if (isset($USER->enrol['tempguest'][$courseid])) {
2116 unset($USER->enrol['tempguest'][$courseid]);
2117 remove_temp_course_roles($context);
2123 * Store user_enrolments changes and trigger event.
2125 * @param stdClass $instance
2126 * @param int $userid
2127 * @param int $status
2128 * @param int $timestart
2129 * @param int $timeend
2130 * @return void
2132 public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2133 global $DB, $USER, $CFG;
2135 $name = $this->get_name();
2137 if ($instance->enrol !== $name) {
2138 throw new coding_exception('invalid enrol instance!');
2141 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2142 // weird, user not enrolled
2143 return;
2146 $modified = false;
2147 if (isset($status) and $ue->status != $status) {
2148 $ue->status = $status;
2149 $modified = true;
2151 if (isset($timestart) and $ue->timestart != $timestart) {
2152 $ue->timestart = $timestart;
2153 $modified = true;
2155 if (isset($timeend) and $ue->timeend != $timeend) {
2156 $ue->timeend = $timeend;
2157 $modified = true;
2160 if (!$modified) {
2161 // no change
2162 return;
2165 $ue->modifierid = $USER->id;
2166 $ue->timemodified = time();
2167 $DB->update_record('user_enrolments', $ue);
2169 // User enrolments have changed, so mark user as dirty.
2170 mark_user_dirty($userid);
2172 // Invalidate core_access cache for get_suspended_userids.
2173 cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2175 // Trigger event.
2176 $event = \core\event\user_enrolment_updated::create(
2177 array(
2178 'objectid' => $ue->id,
2179 'courseid' => $instance->courseid,
2180 'context' => context_course::instance($instance->courseid),
2181 'relateduserid' => $ue->userid,
2182 'other' => array('enrol' => $name)
2185 $event->trigger();
2187 core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2188 $ue->status, $ue->timestart, $ue->timeend);
2192 * Unenrol user from course,
2193 * the last unenrolment removes all remaining roles.
2195 * @param stdClass $instance
2196 * @param int $userid
2197 * @return void
2199 public function unenrol_user(stdClass $instance, $userid) {
2200 global $CFG, $USER, $DB;
2201 require_once("$CFG->dirroot/group/lib.php");
2203 $name = $this->get_name();
2204 $courseid = $instance->courseid;
2206 if ($instance->enrol !== $name) {
2207 throw new coding_exception('invalid enrol instance!');
2209 $context = context_course::instance($instance->courseid, MUST_EXIST);
2211 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2212 // weird, user not enrolled
2213 return;
2216 // Remove all users groups linked to this enrolment instance.
2217 if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2218 foreach ($gms as $gm) {
2219 groups_remove_member($gm->groupid, $gm->userid);
2223 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2224 $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2226 // add extra info and trigger event
2227 $ue->courseid = $courseid;
2228 $ue->enrol = $name;
2230 $sql = "SELECT 'x'
2231 FROM {user_enrolments} ue
2232 JOIN {enrol} e ON (e.id = ue.enrolid)
2233 WHERE ue.userid = :userid AND e.courseid = :courseid";
2234 if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2235 $ue->lastenrol = false;
2237 } else {
2238 // the big cleanup IS necessary!
2239 require_once("$CFG->libdir/gradelib.php");
2241 // remove all remaining roles
2242 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2244 //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2245 groups_delete_group_members($courseid, $userid);
2247 grade_user_unenrol($courseid, $userid);
2249 $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2251 $ue->lastenrol = true; // means user not enrolled any more
2253 // Trigger event.
2254 $event = \core\event\user_enrolment_deleted::create(
2255 array(
2256 'courseid' => $courseid,
2257 'context' => $context,
2258 'relateduserid' => $ue->userid,
2259 'objectid' => $ue->id,
2260 'other' => array(
2261 'userenrolment' => (array)$ue,
2262 'enrol' => $name
2266 $event->trigger();
2268 // User enrolments have changed, so mark user as dirty.
2269 mark_user_dirty($userid);
2271 // Check if courrse contacts cache needs to be cleared.
2272 core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2274 // reset current user enrolment caching
2275 if ($userid == $USER->id) {
2276 if (isset($USER->enrol['enrolled'][$courseid])) {
2277 unset($USER->enrol['enrolled'][$courseid]);
2279 if (isset($USER->enrol['tempguest'][$courseid])) {
2280 unset($USER->enrol['tempguest'][$courseid]);
2281 remove_temp_course_roles($context);
2287 * Forces synchronisation of user enrolments.
2289 * This is important especially for external enrol plugins,
2290 * this function is called for all enabled enrol plugins
2291 * right after every user login.
2293 * @param object $user user record
2294 * @return void
2296 public function sync_user_enrolments($user) {
2297 // override if necessary
2301 * This returns false for backwards compatibility, but it is really recommended.
2303 * @since Moodle 3.1
2304 * @return boolean
2306 public function use_standard_editing_ui() {
2307 return false;
2311 * Return whether or not, given the current state, it is possible to add a new instance
2312 * of this enrolment plugin to the course.
2314 * Default implementation is just for backwards compatibility.
2316 * @param int $courseid
2317 * @return boolean
2319 public function can_add_instance($courseid) {
2320 $link = $this->get_newinstance_link($courseid);
2321 return !empty($link);
2325 * Return whether or not, given the current state, it is possible to edit an instance
2326 * of this enrolment plugin in the course. Used by the standard editing UI
2327 * to generate a link to the edit instance form if editing is allowed.
2329 * @param stdClass $instance
2330 * @return boolean
2332 public function can_edit_instance($instance) {
2333 $context = context_course::instance($instance->courseid);
2335 return has_capability('enrol/' . $instance->enrol . ':config', $context);
2339 * Returns link to page which may be used to add new instance of enrolment plugin in course.
2340 * @param int $courseid
2341 * @return moodle_url page url
2343 public function get_newinstance_link($courseid) {
2344 // override for most plugins, check if instance already exists in cases only one instance is supported
2345 return NULL;
2349 * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2351 public function instance_deleteable($instance) {
2352 throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2353 enrol_plugin::can_delete_instance() instead');
2357 * Is it possible to delete enrol instance via standard UI?
2359 * @param stdClass $instance
2360 * @return bool
2362 public function can_delete_instance($instance) {
2363 return false;
2367 * Is it possible to hide/show enrol instance via standard UI?
2369 * @param stdClass $instance
2370 * @return bool
2372 public function can_hide_show_instance($instance) {
2373 debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2374 return true;
2378 * Returns link to manual enrol UI if exists.
2379 * Does the access control tests automatically.
2381 * @param object $instance
2382 * @return moodle_url
2384 public function get_manual_enrol_link($instance) {
2385 return NULL;
2389 * Returns list of unenrol links for all enrol instances in course.
2391 * @param int $instance
2392 * @return moodle_url or NULL if self unenrolment not supported
2394 public function get_unenrolself_link($instance) {
2395 global $USER, $CFG, $DB;
2397 $name = $this->get_name();
2398 if ($instance->enrol !== $name) {
2399 throw new coding_exception('invalid enrol instance!');
2402 if ($instance->courseid == SITEID) {
2403 return NULL;
2406 if (!enrol_is_enabled($name)) {
2407 return NULL;
2410 if ($instance->status != ENROL_INSTANCE_ENABLED) {
2411 return NULL;
2414 if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2415 return NULL;
2418 $context = context_course::instance($instance->courseid, MUST_EXIST);
2420 if (!has_capability("enrol/$name:unenrolself", $context)) {
2421 return NULL;
2424 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2425 return NULL;
2428 return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2432 * Adds enrol instance UI to course edit form
2434 * @param object $instance enrol instance or null if does not exist yet
2435 * @param MoodleQuickForm $mform
2436 * @param object $data
2437 * @param object $context context of existing course or parent category if course does not exist
2438 * @return void
2440 public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2441 // override - usually at least enable/disable switch, has to add own form header
2445 * Adds form elements to add/edit instance form.
2447 * @since Moodle 3.1
2448 * @param object $instance enrol instance or null if does not exist yet
2449 * @param MoodleQuickForm $mform
2450 * @param context $context
2451 * @return void
2453 public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2454 // Do nothing by default.
2458 * Perform custom validation of the data used to edit the instance.
2460 * @since Moodle 3.1
2461 * @param array $data array of ("fieldname"=>value) of submitted data
2462 * @param array $files array of uploaded files "element_name"=>tmp_file_path
2463 * @param object $instance The instance data loaded from the DB.
2464 * @param context $context The context of the instance we are editing
2465 * @return array of "element_name"=>"error_description" if there are errors,
2466 * or an empty array if everything is OK.
2468 public function edit_instance_validation($data, $files, $instance, $context) {
2469 // No errors by default.
2470 debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2471 return array();
2475 * Validates course edit form data
2477 * @param object $instance enrol instance or null if does not exist yet
2478 * @param array $data
2479 * @param object $context context of existing course or parent category if course does not exist
2480 * @return array errors array
2482 public function course_edit_validation($instance, array $data, $context) {
2483 return array();
2487 * Called after updating/inserting course.
2489 * @param bool $inserted true if course just inserted
2490 * @param object $course
2491 * @param object $data form data
2492 * @return void
2494 public function course_updated($inserted, $course, $data) {
2495 if ($inserted) {
2496 if ($this->get_config('defaultenrol')) {
2497 $this->add_default_instance($course);
2503 * Add new instance of enrol plugin.
2504 * @param object $course
2505 * @param array instance fields
2506 * @return int id of new instance, null if can not be created
2508 public function add_instance($course, array $fields = NULL) {
2509 global $DB;
2511 if ($course->id == SITEID) {
2512 throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2515 $instance = new stdClass();
2516 $instance->enrol = $this->get_name();
2517 $instance->status = ENROL_INSTANCE_ENABLED;
2518 $instance->courseid = $course->id;
2519 $instance->enrolstartdate = 0;
2520 $instance->enrolenddate = 0;
2521 $instance->timemodified = time();
2522 $instance->timecreated = $instance->timemodified;
2523 $instance->sortorder = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2525 $fields = (array)$fields;
2526 unset($fields['enrol']);
2527 unset($fields['courseid']);
2528 unset($fields['sortorder']);
2529 foreach($fields as $field=>$value) {
2530 $instance->$field = $value;
2533 $instance->id = $DB->insert_record('enrol', $instance);
2535 \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2537 return $instance->id;
2541 * Update instance of enrol plugin.
2543 * @since Moodle 3.1
2544 * @param stdClass $instance
2545 * @param stdClass $data modified instance fields
2546 * @return boolean
2548 public function update_instance($instance, $data) {
2549 global $DB;
2550 $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2551 'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2552 'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2553 'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2554 'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2555 'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2557 foreach ($properties as $key) {
2558 if (isset($data->$key)) {
2559 $instance->$key = $data->$key;
2562 $instance->timemodified = time();
2564 $update = $DB->update_record('enrol', $instance);
2565 if ($update) {
2566 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2568 return $update;
2572 * Add new instance of enrol plugin with default settings,
2573 * called when adding new instance manually or when adding new course.
2575 * Not all plugins support this.
2577 * @param object $course
2578 * @return int id of new instance or null if no default supported
2580 public function add_default_instance($course) {
2581 return null;
2585 * Update instance status
2587 * Override when plugin needs to do some action when enabled or disabled.
2589 * @param stdClass $instance
2590 * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2591 * @return void
2593 public function update_status($instance, $newstatus) {
2594 global $DB;
2596 $instance->status = $newstatus;
2597 $DB->update_record('enrol', $instance);
2599 $context = context_course::instance($instance->courseid);
2600 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2602 // Invalidate all enrol caches.
2603 $context->mark_dirty();
2607 * Delete course enrol plugin instance, unenrol all users.
2608 * @param object $instance
2609 * @return void
2611 public function delete_instance($instance) {
2612 global $DB;
2614 $name = $this->get_name();
2615 if ($instance->enrol !== $name) {
2616 throw new coding_exception('invalid enrol instance!');
2619 //first unenrol all users
2620 $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2621 foreach ($participants as $participant) {
2622 $this->unenrol_user($instance, $participant->userid);
2624 $participants->close();
2626 // now clean up all remainders that were not removed correctly
2627 if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2628 foreach ($gms as $gm) {
2629 groups_remove_member($gm->groupid, $gm->userid);
2632 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2633 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2635 // finally drop the enrol row
2636 $DB->delete_records('enrol', array('id'=>$instance->id));
2638 $context = context_course::instance($instance->courseid);
2639 \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2641 // Invalidate all enrol caches.
2642 $context->mark_dirty();
2646 * Creates course enrol form, checks if form submitted
2647 * and enrols user if necessary. It can also redirect.
2649 * @param stdClass $instance
2650 * @return string html text, usually a form in a text box
2652 public function enrol_page_hook(stdClass $instance) {
2653 return null;
2657 * Checks if user can self enrol.
2659 * @param stdClass $instance enrolment instance
2660 * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2661 * used by navigation to improve performance.
2662 * @return bool|string true if successful, else error message or false
2664 public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2665 return false;
2669 * Return information for enrolment instance containing list of parameters required
2670 * for enrolment, name of enrolment plugin etc.
2672 * @param stdClass $instance enrolment instance
2673 * @return array instance info.
2675 public function get_enrol_info(stdClass $instance) {
2676 return null;
2680 * Adds navigation links into course admin block.
2682 * By defaults looks for manage links only.
2684 * @param navigation_node $instancesnode
2685 * @param stdClass $instance
2686 * @return void
2688 public function add_course_navigation($instancesnode, stdClass $instance) {
2689 if ($this->use_standard_editing_ui()) {
2690 $context = context_course::instance($instance->courseid);
2691 $cap = 'enrol/' . $instance->enrol . ':config';
2692 if (has_capability($cap, $context)) {
2693 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2694 $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2695 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2701 * Returns edit icons for the page with list of instances
2702 * @param stdClass $instance
2703 * @return array
2705 public function get_action_icons(stdClass $instance) {
2706 global $OUTPUT;
2708 $icons = array();
2709 if ($this->use_standard_editing_ui()) {
2710 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2711 $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2712 $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2713 array('class' => 'iconsmall')));
2715 return $icons;
2719 * Reads version.php and determines if it is necessary
2720 * to execute the cron job now.
2721 * @return bool
2723 public function is_cron_required() {
2724 global $CFG;
2726 $name = $this->get_name();
2727 $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2728 $plugin = new stdClass();
2729 include($versionfile);
2730 if (empty($plugin->cron)) {
2731 return false;
2733 $lastexecuted = $this->get_config('lastcron', 0);
2734 if ($lastexecuted + $plugin->cron < time()) {
2735 return true;
2736 } else {
2737 return false;
2742 * Called for all enabled enrol plugins that returned true from is_cron_required().
2743 * @return void
2745 public function cron() {
2749 * Called when user is about to be deleted
2750 * @param object $user
2751 * @return void
2753 public function user_delete($user) {
2754 global $DB;
2756 $sql = "SELECT e.*
2757 FROM {enrol} e
2758 JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2759 WHERE e.enrol = :name AND ue.userid = :userid";
2760 $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2762 $rs = $DB->get_recordset_sql($sql, $params);
2763 foreach($rs as $instance) {
2764 $this->unenrol_user($instance, $user->id);
2766 $rs->close();
2770 * Returns an enrol_user_button that takes the user to a page where they are able to
2771 * enrol users into the managers course through this plugin.
2773 * Optional: If the plugin supports manual enrolments it can choose to override this
2774 * otherwise it shouldn't
2776 * @param course_enrolment_manager $manager
2777 * @return enrol_user_button|false
2779 public function get_manual_enrol_button(course_enrolment_manager $manager) {
2780 return false;
2784 * Gets an array of the user enrolment actions
2786 * @param course_enrolment_manager $manager
2787 * @param stdClass $ue
2788 * @return array An array of user_enrolment_actions
2790 public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2791 $actions = [];
2792 $context = $manager->get_context();
2793 $instance = $ue->enrolmentinstance;
2794 $params = $manager->get_moodlepage()->url->params();
2795 $params['ue'] = $ue->id;
2797 // Edit enrolment action.
2798 if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2799 $title = get_string('editenrolment', 'enrol');
2800 $icon = new pix_icon('t/edit', $title);
2801 $url = new moodle_url('/enrol/editenrolment.php', $params);
2802 $actionparams = [
2803 'class' => 'editenrollink',
2804 'rel' => $ue->id,
2805 'data-action' => ENROL_ACTION_EDIT
2807 $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2810 // Unenrol action.
2811 if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2812 $title = get_string('unenrol', 'enrol');
2813 $icon = new pix_icon('t/delete', $title);
2814 $url = new moodle_url('/enrol/unenroluser.php', $params);
2815 $actionparams = [
2816 'class' => 'unenrollink',
2817 'rel' => $ue->id,
2818 'data-action' => ENROL_ACTION_UNENROL
2820 $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2822 return $actions;
2826 * Returns true if the plugin has one or more bulk operations that can be performed on
2827 * user enrolments.
2829 * @param course_enrolment_manager $manager
2830 * @return bool
2832 public function has_bulk_operations(course_enrolment_manager $manager) {
2833 return false;
2837 * Return an array of enrol_bulk_enrolment_operation objects that define
2838 * the bulk actions that can be performed on user enrolments by the plugin.
2840 * @param course_enrolment_manager $manager
2841 * @return array
2843 public function get_bulk_operations(course_enrolment_manager $manager) {
2844 return array();
2848 * Do any enrolments need expiration processing.
2850 * Plugins that want to call this functionality must implement 'expiredaction' config setting.
2852 * @param progress_trace $trace
2853 * @param int $courseid one course, empty mean all
2854 * @return bool true if any data processed, false if not
2856 public function process_expirations(progress_trace $trace, $courseid = null) {
2857 global $DB;
2859 $name = $this->get_name();
2860 if (!enrol_is_enabled($name)) {
2861 $trace->finished();
2862 return false;
2865 $processed = false;
2866 $params = array();
2867 $coursesql = "";
2868 if ($courseid) {
2869 $coursesql = "AND e.courseid = :courseid";
2872 // Deal with expired accounts.
2873 $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
2875 if ($action == ENROL_EXT_REMOVED_UNENROL) {
2876 $instances = array();
2877 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2878 FROM {user_enrolments} ue
2879 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2880 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2881 WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
2882 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
2884 $rs = $DB->get_recordset_sql($sql, $params);
2885 foreach ($rs as $ue) {
2886 if (!$processed) {
2887 $trace->output("Starting processing of enrol_$name expirations...");
2888 $processed = true;
2890 if (empty($instances[$ue->enrolid])) {
2891 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2893 $instance = $instances[$ue->enrolid];
2894 if (!$this->roles_protected()) {
2895 // Let's just guess what extra roles are supposed to be removed.
2896 if ($instance->roleid) {
2897 role_unassign($instance->roleid, $ue->userid, $ue->contextid);
2900 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
2901 $this->unenrol_user($instance, $ue->userid);
2902 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
2904 $rs->close();
2905 unset($instances);
2907 } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
2908 $instances = array();
2909 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2910 FROM {user_enrolments} ue
2911 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2912 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2913 WHERE ue.timeend > 0 AND ue.timeend < :now
2914 AND ue.status = :useractive $coursesql";
2915 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
2916 $rs = $DB->get_recordset_sql($sql, $params);
2917 foreach ($rs as $ue) {
2918 if (!$processed) {
2919 $trace->output("Starting processing of enrol_$name expirations...");
2920 $processed = true;
2922 if (empty($instances[$ue->enrolid])) {
2923 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2925 $instance = $instances[$ue->enrolid];
2927 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
2928 if (!$this->roles_protected()) {
2929 // Let's just guess what roles should be removed.
2930 $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
2931 if ($count == 1) {
2932 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
2934 } else if ($count > 1 and $instance->roleid) {
2935 role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
2938 // In any case remove all roles that belong to this instance and user.
2939 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
2940 // Final cleanup of subcontexts if there are no more course roles.
2941 if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
2942 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
2946 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
2947 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
2949 $rs->close();
2950 unset($instances);
2952 } else {
2953 // ENROL_EXT_REMOVED_KEEP means no changes.
2956 if ($processed) {
2957 $trace->output("...finished processing of enrol_$name expirations");
2958 } else {
2959 $trace->output("No expired enrol_$name enrolments detected");
2961 $trace->finished();
2963 return $processed;
2967 * Send expiry notifications.
2969 * Plugin that wants to have expiry notification MUST implement following:
2970 * - expirynotifyhour plugin setting,
2971 * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
2972 * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
2973 * expirymessageenrolledsubject and expirymessageenrolledbody),
2974 * - expiry_notification provider in db/messages.php,
2975 * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2976 * - something that calls this method, such as cron.
2978 * @param progress_trace $trace (accepts bool for backwards compatibility only)
2980 public function send_expiry_notifications($trace) {
2981 global $DB, $CFG;
2983 $name = $this->get_name();
2984 if (!enrol_is_enabled($name)) {
2985 $trace->finished();
2986 return;
2989 // Unfortunately this may take a long time, it should not be interrupted,
2990 // otherwise users get duplicate notification.
2992 core_php_time_limit::raise();
2993 raise_memory_limit(MEMORY_HUGE);
2996 $expirynotifylast = $this->get_config('expirynotifylast', 0);
2997 $expirynotifyhour = $this->get_config('expirynotifyhour');
2998 if (is_null($expirynotifyhour)) {
2999 debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
3000 $trace->finished();
3001 return;
3004 if (!($trace instanceof progress_trace)) {
3005 $trace = $trace ? new text_progress_trace() : new null_progress_trace();
3006 debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
3009 $timenow = time();
3010 $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
3012 if ($expirynotifylast > $notifytime) {
3013 $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
3014 $trace->finished();
3015 return;
3017 } else if ($timenow < $notifytime) {
3018 $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
3019 $trace->finished();
3020 return;
3023 $trace->output('Processing '.$name.' enrolment expiration notifications...');
3025 // Notify users responsible for enrolment once every day.
3026 $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
3027 FROM {user_enrolments} ue
3028 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
3029 JOIN {course} c ON (c.id = e.courseid)
3030 JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
3031 WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
3032 ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
3033 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
3035 $rs = $DB->get_recordset_sql($sql, $params);
3037 $lastenrollid = 0;
3038 $users = array();
3040 foreach($rs as $ue) {
3041 if ($lastenrollid and $lastenrollid != $ue->enrolid) {
3042 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3043 $users = array();
3045 $lastenrollid = $ue->enrolid;
3047 $enroller = $this->get_enroller($ue->enrolid);
3048 $context = context_course::instance($ue->courseid);
3050 $user = $DB->get_record('user', array('id'=>$ue->userid));
3052 $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
3054 if (!$ue->notifyall) {
3055 continue;
3058 if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
3059 // Notify enrolled users only once at the start of the threshold.
3060 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3061 continue;
3064 $this->notify_expiry_enrolled($user, $ue, $trace);
3066 $rs->close();
3068 if ($lastenrollid and $users) {
3069 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3072 $trace->output('...notification processing finished.');
3073 $trace->finished();
3075 $this->set_config('expirynotifylast', $timenow);
3079 * Returns the user who is responsible for enrolments for given instance.
3081 * Override if plugin knows anybody better than admin.
3083 * @param int $instanceid enrolment instance id
3084 * @return stdClass user record
3086 protected function get_enroller($instanceid) {
3087 return get_admin();
3091 * Notify user about incoming expiration of their enrolment,
3092 * it is called only if notification of enrolled users (aka students) is enabled in course.
3094 * This is executed only once for each expiring enrolment right
3095 * at the start of the expiration threshold.
3097 * @param stdClass $user
3098 * @param stdClass $ue
3099 * @param progress_trace $trace
3101 protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3102 global $CFG;
3104 $name = $this->get_name();
3106 $oldforcelang = force_current_language($user->lang);
3108 $enroller = $this->get_enroller($ue->enrolid);
3109 $context = context_course::instance($ue->courseid);
3111 $a = new stdClass();
3112 $a->course = format_string($ue->fullname, true, array('context'=>$context));
3113 $a->user = fullname($user, true);
3114 $a->timeend = userdate($ue->timeend, '', $user->timezone);
3115 $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3117 $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
3118 $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3120 $message = new \core\message\message();
3121 $message->courseid = $ue->courseid;
3122 $message->notification = 1;
3123 $message->component = 'enrol_'.$name;
3124 $message->name = 'expiry_notification';
3125 $message->userfrom = $enroller;
3126 $message->userto = $user;
3127 $message->subject = $subject;
3128 $message->fullmessage = $body;
3129 $message->fullmessageformat = FORMAT_MARKDOWN;
3130 $message->fullmessagehtml = markdown_to_html($body);
3131 $message->smallmessage = $subject;
3132 $message->contexturlname = $a->course;
3133 $message->contexturl = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
3135 if (message_send($message)) {
3136 $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3137 } else {
3138 $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
3141 force_current_language($oldforcelang);
3145 * Notify person responsible for enrolments that some user enrolments will be expired soon,
3146 * it is called only if notification of enrollers (aka teachers) is enabled in course.
3148 * This is called repeatedly every day for each course if there are any pending expiration
3149 * in the expiration threshold.
3151 * @param int $eid
3152 * @param array $users
3153 * @param progress_trace $trace
3155 protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3156 global $DB;
3158 $name = $this->get_name();
3160 $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3161 $context = context_course::instance($instance->courseid);
3162 $course = $DB->get_record('course', array('id'=>$instance->courseid));
3164 $enroller = $this->get_enroller($instance->id);
3165 $admin = get_admin();
3167 $oldforcelang = force_current_language($enroller->lang);
3169 foreach($users as $key=>$info) {
3170 $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3173 $a = new stdClass();
3174 $a->course = format_string($course->fullname, true, array('context'=>$context));
3175 $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3176 $a->users = implode("\n", $users);
3177 $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3179 $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3180 $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3182 $message = new \core\message\message();
3183 $message->courseid = $course->id;
3184 $message->notification = 1;
3185 $message->component = 'enrol_'.$name;
3186 $message->name = 'expiry_notification';
3187 $message->userfrom = $admin;
3188 $message->userto = $enroller;
3189 $message->subject = $subject;
3190 $message->fullmessage = $body;
3191 $message->fullmessageformat = FORMAT_MARKDOWN;
3192 $message->fullmessagehtml = markdown_to_html($body);
3193 $message->smallmessage = $subject;
3194 $message->contexturlname = $a->course;
3195 $message->contexturl = $a->extendurl;
3197 if (message_send($message)) {
3198 $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3199 } else {
3200 $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3203 force_current_language($oldforcelang);
3207 * Backup execution step hook to annotate custom fields.
3209 * @param backup_enrolments_execution_step $step
3210 * @param stdClass $enrol
3212 public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3213 // Override as necessary to annotate custom fields in the enrol table.
3217 * Automatic enrol sync executed during restore.
3218 * Useful for automatic sync by course->idnumber or course category.
3219 * @param stdClass $course course record
3221 public function restore_sync_course($course) {
3222 // Override if necessary.
3226 * Restore instance and map settings.
3228 * @param restore_enrolments_structure_step $step
3229 * @param stdClass $data
3230 * @param stdClass $course
3231 * @param int $oldid
3233 public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3234 // Do not call this from overridden methods, restore and set new id there.
3235 $step->set_mapping('enrol', $oldid, 0);
3239 * Restore user enrolment.
3241 * @param restore_enrolments_structure_step $step
3242 * @param stdClass $data
3243 * @param stdClass $instance
3244 * @param int $oldinstancestatus
3245 * @param int $userid
3247 public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3248 // Override as necessary if plugin supports restore of enrolments.
3252 * Restore role assignment.
3254 * @param stdClass $instance
3255 * @param int $roleid
3256 * @param int $userid
3257 * @param int $contextid
3259 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3260 // No role assignment by default, override if necessary.
3264 * Restore user group membership.
3265 * @param stdClass $instance
3266 * @param int $groupid
3267 * @param int $userid
3269 public function restore_group_member($instance, $groupid, $userid) {
3270 // Implement if you want to restore protected group memberships,
3271 // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3275 * Returns defaults for new instances.
3276 * @since Moodle 3.1
3277 * @return array
3279 public function get_instance_defaults() {
3280 return array();
3284 * Validate a list of parameter names and types.
3285 * @since Moodle 3.1
3287 * @param array $data array of ("fieldname"=>value) of submitted data
3288 * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3289 * @return array of "element_name"=>"error_description" if there are errors,
3290 * or an empty array if everything is OK.
3292 public function validate_param_types($data, $rules) {
3293 $errors = array();
3294 $invalidstr = get_string('invaliddata', 'error');
3295 foreach ($rules as $fieldname => $rule) {
3296 if (is_array($rule)) {
3297 if (!in_array($data[$fieldname], $rule)) {
3298 $errors[$fieldname] = $invalidstr;
3300 } else {
3301 if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3302 $errors[$fieldname] = $invalidstr;
3306 return $errors;