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