MDL-53169 database: use bound empty strings for cross-db
[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  * - $fields is an array of field names to ADD
548  *   so name the fields you really need, which will
549  *   be added and uniq'd
550  *
551  * If $allaccessible is true, this will additionally return courses that the current user is not
552  * enrolled in, but can access because they are open to the user for other reasons (course view
553  * permission, currently viewing course as a guest, or course allows guest access without
554  * password).
555  *
556  * @param string|array $fields
557  * @param string $sort
558  * @param int $limit max number of courses
559  * @param array $courseids the list of course ids to filter by
560  * @param bool $allaccessible Include courses user is not enrolled in, but can access
561  * @return array
562  */
563 function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
564           $limit = 0, $courseids = [], $allaccessible = false) {
565     global $DB, $USER, $CFG;
567     // Guest account does not have any enrolled courses.
568     if (!$allaccessible && (isguestuser() or !isloggedin())) {
569         return array();
570     }
572     $basefields = array('id', 'category', 'sortorder',
573                         'shortname', 'fullname', 'idnumber',
574                         'startdate', 'visible',
575                         'groupmode', 'groupmodeforce', 'cacherev');
577     if (empty($fields)) {
578         $fields = $basefields;
579     } else if (is_string($fields)) {
580         // turn the fields from a string to an array
581         $fields = explode(',', $fields);
582         $fields = array_map('trim', $fields);
583         $fields = array_unique(array_merge($basefields, $fields));
584     } else if (is_array($fields)) {
585         $fields = array_unique(array_merge($basefields, $fields));
586     } else {
587         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
588     }
589     if (in_array('*', $fields)) {
590         $fields = array('*');
591     }
593     $orderby = "";
594     $sort    = trim($sort);
595     if (!empty($sort)) {
596         $rawsorts = explode(',', $sort);
597         $sorts = array();
598         foreach ($rawsorts as $rawsort) {
599             $rawsort = trim($rawsort);
600             if (strpos($rawsort, 'c.') === 0) {
601                 $rawsort = substr($rawsort, 2);
602             }
603             $sorts[] = trim($rawsort);
604         }
605         $sort = 'c.'.implode(',c.', $sorts);
606         $orderby = "ORDER BY $sort";
607     }
609     $wheres = array("c.id <> :siteid");
610     $params = array('siteid'=>SITEID);
612     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
613         // list _only_ this course - anything else is asking for trouble...
614         $wheres[] = "courseid = :loginas";
615         $params['loginas'] = $USER->loginascontext->instanceid;
616     }
618     $coursefields = 'c.' .join(',c.', $fields);
619     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
620     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
621     $params['contextlevel'] = CONTEXT_COURSE;
622     $wheres = implode(" AND ", $wheres);
624     if (!empty($courseids)) {
625         list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
626         $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
627         $params = array_merge($params, $courseidsparams);
628     }
630     $courseidsql = "";
631     // Logged-in, non-guest users get their enrolled courses.
632     if (!isguestuser() && isloggedin()) {
633         $courseidsql .= "
634                 SELECT DISTINCT e.courseid
635                   FROM {enrol} e
636                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
637                  WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1
638                        AND (ue.timeend = 0 OR ue.timeend > :now2)";
639         $params['userid'] = $USER->id;
640         $params['active'] = ENROL_USER_ACTIVE;
641         $params['enabled'] = ENROL_INSTANCE_ENABLED;
642         $params['now1'] = round(time(), -2); // Improves db caching.
643         $params['now2'] = $params['now1'];
644     }
646     // When including non-enrolled but accessible courses...
647     if ($allaccessible) {
648         if (is_siteadmin()) {
649             // Site admins can access all courses.
650             $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
651         } else {
652             // If we used the enrolment as well, then this will be UNIONed.
653             if ($courseidsql) {
654                 $courseidsql .= " UNION ";
655             }
657             // Include courses with guest access and no password.
658             $courseidsql .= "
659                     SELECT DISTINCT e.courseid
660                       FROM {enrol} e
661                      WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
662             $params['emptypass'] = '';
663             $params['enabled2'] = ENROL_INSTANCE_ENABLED;
665             // Include courses where the current user is currently using guest access (may include
666             // those which require a password).
667             $courseids = [];
668             $accessdata = get_user_accessdata($USER->id);
669             foreach ($accessdata['ra'] as $contextpath => $roles) {
670                 if (array_key_exists($CFG->guestroleid, $roles)) {
671                     // Work out the course id from context path.
672                     $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
673                     if ($context instanceof context_course) {
674                         $courseids[$context->instanceid] = true;
675                     }
676                 }
677             }
679             // Include courses where the current user has moodle/course:view capability.
680             $courses = get_user_capability_course('moodle/course:view', null, false);
681             if (!$courses) {
682                 $courses = [];
683             }
684             foreach ($courses as $course) {
685                 $courseids[$course->id] = true;
686             }
688             // If there are any in either category, list them individually.
689             if ($courseids) {
690                 list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
691                         array_keys($courseids), SQL_PARAMS_NAMED);
692                 $courseidsql .= "
693                         UNION
694                        SELECT DISTINCT c3.id AS courseid
695                          FROM {course} c3
696                         WHERE c3.id $allowedsql";
697                 $params = array_merge($params, $allowedparams);
698             }
699         }
700     }
702     // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
703     // we have the subselect there.
704     $sql = "SELECT $coursefields $ccselect
705               FROM {course} c
706               JOIN ($courseidsql) en ON (en.courseid = c.id)
707            $ccjoin
708              WHERE $wheres
709           $orderby";
711     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
713     // preload contexts and check visibility
714     foreach ($courses as $id=>$course) {
715         context_helper::preload_from_record($course);
716         if (!$course->visible) {
717             if (!$context = context_course::instance($id, IGNORE_MISSING)) {
718                 unset($courses[$id]);
719                 continue;
720             }
721             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
722                 unset($courses[$id]);
723                 continue;
724             }
725         }
726         $courses[$id] = $course;
727     }
729     //wow! Is that really all? :-D
731     return $courses;
734 /**
735  * Returns course enrolment information icons.
736  *
737  * @param object $course
738  * @param array $instances enrol instances of this course, improves performance
739  * @return array of pix_icon
740  */
741 function enrol_get_course_info_icons($course, array $instances = NULL) {
742     $icons = array();
743     if (is_null($instances)) {
744         $instances = enrol_get_instances($course->id, true);
745     }
746     $plugins = enrol_get_plugins(true);
747     foreach ($plugins as $name => $plugin) {
748         $pis = array();
749         foreach ($instances as $instance) {
750             if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
751                 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
752                 continue;
753             }
754             if ($instance->enrol == $name) {
755                 $pis[$instance->id] = $instance;
756             }
757         }
758         if ($pis) {
759             $icons = array_merge($icons, $plugin->get_info_icons($pis));
760         }
761     }
762     return $icons;
765 /**
766  * Returns course enrolment detailed information.
767  *
768  * @param object $course
769  * @return array of html fragments - can be used to construct lists
770  */
771 function enrol_get_course_description_texts($course) {
772     $lines = array();
773     $instances = enrol_get_instances($course->id, true);
774     $plugins = enrol_get_plugins(true);
775     foreach ($instances as $instance) {
776         if (!isset($plugins[$instance->enrol])) {
777             //weird
778             continue;
779         }
780         $plugin = $plugins[$instance->enrol];
781         $text = $plugin->get_description_text($instance);
782         if ($text !== NULL) {
783             $lines[] = $text;
784         }
785     }
786     return $lines;
789 /**
790  * Returns list of courses user is enrolled into.
791  * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
792  *
793  * - $fields is an array of fieldnames to ADD
794  *   so name the fields you really need, which will
795  *   be added and uniq'd
796  *
797  * @param int $userid
798  * @param bool $onlyactive return only active enrolments in courses user may see
799  * @param string|array $fields
800  * @param string $sort
801  * @return array
802  */
803 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
804     global $DB;
806     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
808     // preload contexts and check visibility
809     if ($onlyactive) {
810         foreach ($courses as $id=>$course) {
811             context_helper::preload_from_record($course);
812             if (!$course->visible) {
813                 if (!$context = context_course::instance($id)) {
814                     unset($courses[$id]);
815                     continue;
816                 }
817                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
818                     unset($courses[$id]);
819                     continue;
820                 }
821             }
822         }
823     }
825     return $courses;
829 /**
830  * Can user access at least one enrolled course?
831  *
832  * Cheat if necessary, but find out as fast as possible!
833  *
834  * @param int|stdClass $user null means use current user
835  * @return bool
836  */
837 function enrol_user_sees_own_courses($user = null) {
838     global $USER;
840     if ($user === null) {
841         $user = $USER;
842     }
843     $userid = is_object($user) ? $user->id : $user;
845     // Guest account does not have any courses
846     if (isguestuser($userid) or empty($userid)) {
847         return false;
848     }
850     // Let's cheat here if this is the current user,
851     // if user accessed any course recently, then most probably
852     // we do not need to query the database at all.
853     if ($USER->id == $userid) {
854         if (!empty($USER->enrol['enrolled'])) {
855             foreach ($USER->enrol['enrolled'] as $until) {
856                 if ($until > time()) {
857                     return true;
858                 }
859             }
860         }
861     }
863     // Now the slow way.
864     $courses = enrol_get_all_users_courses($userid, true);
865     foreach($courses as $course) {
866         if ($course->visible) {
867             return true;
868         }
869         context_helper::preload_from_record($course);
870         $context = context_course::instance($course->id);
871         if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
872             return true;
873         }
874     }
876     return false;
879 /**
880  * Returns list of courses user is enrolled into without any capability checks
881  * - $fields is an array of fieldnames to ADD
882  *   so name the fields you really need, which will
883  *   be added and uniq'd
884  *
885  * @param int $userid
886  * @param bool $onlyactive return only active enrolments in courses user may see
887  * @param string|array $fields
888  * @param string $sort
889  * @return array
890  */
891 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
892     global $DB;
894     // Guest account does not have any courses
895     if (isguestuser($userid) or empty($userid)) {
896         return(array());
897     }
899     $basefields = array('id', 'category', 'sortorder',
900             'shortname', 'fullname', 'idnumber',
901             'startdate', 'visible',
902             'defaultgroupingid',
903             'groupmode', 'groupmodeforce');
905     if (empty($fields)) {
906         $fields = $basefields;
907     } else if (is_string($fields)) {
908         // turn the fields from a string to an array
909         $fields = explode(',', $fields);
910         $fields = array_map('trim', $fields);
911         $fields = array_unique(array_merge($basefields, $fields));
912     } else if (is_array($fields)) {
913         $fields = array_unique(array_merge($basefields, $fields));
914     } else {
915         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
916     }
917     if (in_array('*', $fields)) {
918         $fields = array('*');
919     }
921     $orderby = "";
922     $sort    = trim($sort);
923     if (!empty($sort)) {
924         $rawsorts = explode(',', $sort);
925         $sorts = array();
926         foreach ($rawsorts as $rawsort) {
927             $rawsort = trim($rawsort);
928             if (strpos($rawsort, 'c.') === 0) {
929                 $rawsort = substr($rawsort, 2);
930             }
931             $sorts[] = trim($rawsort);
932         }
933         $sort = 'c.'.implode(',c.', $sorts);
934         $orderby = "ORDER BY $sort";
935     }
937     $params = array('siteid'=>SITEID);
939     if ($onlyactive) {
940         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
941         $params['now1']    = round(time(), -2); // improves db caching
942         $params['now2']    = $params['now1'];
943         $params['active']  = ENROL_USER_ACTIVE;
944         $params['enabled'] = ENROL_INSTANCE_ENABLED;
945     } else {
946         $subwhere = "";
947     }
949     $coursefields = 'c.' .join(',c.', $fields);
950     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
951     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
952     $params['contextlevel'] = CONTEXT_COURSE;
954     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
955     $sql = "SELECT $coursefields $ccselect
956               FROM {course} c
957               JOIN (SELECT DISTINCT e.courseid
958                       FROM {enrol} e
959                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
960                  $subwhere
961                    ) en ON (en.courseid = c.id)
962            $ccjoin
963              WHERE c.id <> :siteid
964           $orderby";
965     $params['userid']  = $userid;
967     $courses = $DB->get_records_sql($sql, $params);
969     return $courses;
974 /**
975  * Called when user is about to be deleted.
976  * @param object $user
977  * @return void
978  */
979 function enrol_user_delete($user) {
980     global $DB;
982     $plugins = enrol_get_plugins(true);
983     foreach ($plugins as $plugin) {
984         $plugin->user_delete($user);
985     }
987     // force cleanup of all broken enrolments
988     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
991 /**
992  * Called when course is about to be deleted.
993  * @param stdClass $course
994  * @return void
995  */
996 function enrol_course_delete($course) {
997     global $DB;
999     $instances = enrol_get_instances($course->id, false);
1000     $plugins = enrol_get_plugins(true);
1001     foreach ($instances as $instance) {
1002         if (isset($plugins[$instance->enrol])) {
1003             $plugins[$instance->enrol]->delete_instance($instance);
1004         }
1005         // low level delete in case plugin did not do it
1006         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1007         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1008         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1009         $DB->delete_records('enrol', array('id'=>$instance->id));
1010     }
1013 /**
1014  * Try to enrol user via default internal auth plugin.
1015  *
1016  * For now this is always using the manual enrol plugin...
1017  *
1018  * @param $courseid
1019  * @param $userid
1020  * @param $roleid
1021  * @param $timestart
1022  * @param $timeend
1023  * @return bool success
1024  */
1025 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1026     global $DB;
1028     //note: this is hardcoded to manual plugin for now
1030     if (!enrol_is_enabled('manual')) {
1031         return false;
1032     }
1034     if (!$enrol = enrol_get_plugin('manual')) {
1035         return false;
1036     }
1037     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1038         return false;
1039     }
1040     $instance = reset($instances);
1042     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1044     return true;
1047 /**
1048  * Is there a chance users might self enrol
1049  * @param int $courseid
1050  * @return bool
1051  */
1052 function enrol_selfenrol_available($courseid) {
1053     $result = false;
1055     $plugins = enrol_get_plugins(true);
1056     $enrolinstances = enrol_get_instances($courseid, true);
1057     foreach($enrolinstances as $instance) {
1058         if (!isset($plugins[$instance->enrol])) {
1059             continue;
1060         }
1061         if ($instance->enrol === 'guest') {
1062             // blacklist known temporary guest plugins
1063             continue;
1064         }
1065         if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
1066             $result = true;
1067             break;
1068         }
1069     }
1071     return $result;
1074 /**
1075  * This function returns the end of current active user enrolment.
1076  *
1077  * It deals correctly with multiple overlapping user enrolments.
1078  *
1079  * @param int $courseid
1080  * @param int $userid
1081  * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1082  */
1083 function enrol_get_enrolment_end($courseid, $userid) {
1084     global $DB;
1086     $sql = "SELECT ue.*
1087               FROM {user_enrolments} ue
1088               JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1089               JOIN {user} u ON u.id = ue.userid
1090              WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1091     $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1093     if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1094         return false;
1095     }
1097     $changes = array();
1099     foreach ($enrolments as $ue) {
1100         $start = (int)$ue->timestart;
1101         $end = (int)$ue->timeend;
1102         if ($end != 0 and $end < $start) {
1103             debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1104             continue;
1105         }
1106         if (isset($changes[$start])) {
1107             $changes[$start] = $changes[$start] + 1;
1108         } else {
1109             $changes[$start] = 1;
1110         }
1111         if ($end === 0) {
1112             // no end
1113         } else if (isset($changes[$end])) {
1114             $changes[$end] = $changes[$end] - 1;
1115         } else {
1116             $changes[$end] = -1;
1117         }
1118     }
1120     // let's sort then enrolment starts&ends and go through them chronologically,
1121     // looking for current status and the next future end of enrolment
1122     ksort($changes);
1124     $now = time();
1125     $current = 0;
1126     $present = null;
1128     foreach ($changes as $time => $change) {
1129         if ($time > $now) {
1130             if ($present === null) {
1131                 // we have just went past current time
1132                 $present = $current;
1133                 if ($present < 1) {
1134                     // no enrolment active
1135                     return false;
1136                 }
1137             }
1138             if ($present !== null) {
1139                 // we are already in the future - look for possible end
1140                 if ($current + $change < 1) {
1141                     return $time;
1142                 }
1143             }
1144         }
1145         $current += $change;
1146     }
1148     if ($current > 0) {
1149         return 0;
1150     } else {
1151         return false;
1152     }
1155 /**
1156  * Is current user accessing course via this enrolment method?
1157  *
1158  * This is intended for operations that are going to affect enrol instances.
1159  *
1160  * @param stdClass $instance enrol instance
1161  * @return bool
1162  */
1163 function enrol_accessing_via_instance(stdClass $instance) {
1164     global $DB, $USER;
1166     if (empty($instance->id)) {
1167         return false;
1168     }
1170     if (is_siteadmin()) {
1171         // Admins may go anywhere.
1172         return false;
1173     }
1175     return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1178 /**
1179  * Returns true if user is enrolled (is participating) in course
1180  * this is intended for students and teachers.
1181  *
1182  * Since 2.2 the result for active enrolments and current user are cached.
1183  *
1184  * @param context $context
1185  * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1186  * @param string $withcapability extra capability name
1187  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1188  * @return bool
1189  */
1190 function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1191     global $USER, $DB;
1193     // First find the course context.
1194     $coursecontext = $context->get_course_context();
1196     // Make sure there is a real user specified.
1197     if ($user === null) {
1198         $userid = isset($USER->id) ? $USER->id : 0;
1199     } else {
1200         $userid = is_object($user) ? $user->id : $user;
1201     }
1203     if (empty($userid)) {
1204         // Not-logged-in!
1205         return false;
1206     } else if (isguestuser($userid)) {
1207         // Guest account can not be enrolled anywhere.
1208         return false;
1209     }
1211     // Note everybody participates on frontpage, so for other contexts...
1212     if ($coursecontext->instanceid != SITEID) {
1213         // Try cached info first - the enrolled flag is set only when active enrolment present.
1214         if ($USER->id == $userid) {
1215             $coursecontext->reload_if_dirty();
1216             if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1217                 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1218                     if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1219                         return false;
1220                     }
1221                     return true;
1222                 }
1223             }
1224         }
1226         if ($onlyactive) {
1227             // Look for active enrolments only.
1228             $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1230             if ($until === false) {
1231                 return false;
1232             }
1234             if ($USER->id == $userid) {
1235                 if ($until == 0) {
1236                     $until = ENROL_MAX_TIMESTAMP;
1237                 }
1238                 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1239                 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1240                     unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1241                     remove_temp_course_roles($coursecontext);
1242                 }
1243             }
1245         } else {
1246             // Any enrolment is good for us here, even outdated, disabled or inactive.
1247             $sql = "SELECT 'x'
1248                       FROM {user_enrolments} ue
1249                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1250                       JOIN {user} u ON u.id = ue.userid
1251                      WHERE ue.userid = :userid AND u.deleted = 0";
1252             $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1253             if (!$DB->record_exists_sql($sql, $params)) {
1254                 return false;
1255             }
1256         }
1257     }
1259     if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1260         return false;
1261     }
1263     return true;
1266 /**
1267  * Returns an array of joins, wheres and params that will limit the group of
1268  * users to only those enrolled and with given capability (if specified).
1269  *
1270  * Note this join will return duplicate rows for users who have been enrolled
1271  * several times (e.g. as manual enrolment, and as self enrolment). You may
1272  * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1273  *
1274  * @param context $context
1275  * @param string $prefix optional, a prefix to the user id column
1276  * @param string|array $capability optional, may include a capability name, or array of names.
1277  *      If an array is provided then this is the equivalent of a logical 'OR',
1278  *      i.e. the user needs to have one of these capabilities.
1279  * @param int $group optional, 0 indicates no current group, otherwise the group id
1280  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1281  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1282  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1283  * @return \core\dml\sql_join Contains joins, wheres, params
1284  */
1285 function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
1286         $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1287     $uid = $prefix . 'u.id';
1288     $joins = array();
1289     $wheres = array();
1291     $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1292     $joins[] = $enrolledjoin->joins;
1293     $wheres[] = $enrolledjoin->wheres;
1294     $params = $enrolledjoin->params;
1296     if (!empty($capability)) {
1297         $capjoin = get_with_capability_join($context, $capability, $uid);
1298         $joins[] = $capjoin->joins;
1299         $wheres[] = $capjoin->wheres;
1300         $params = array_merge($params, $capjoin->params);
1301     }
1303     if ($group) {
1304         $groupjoin = groups_get_members_join($group, $uid);
1305         $joins[] = $groupjoin->joins;
1306         $params = array_merge($params, $groupjoin->params);
1307     }
1309     $joins = implode("\n", $joins);
1310     $wheres[] = "{$prefix}u.deleted = 0";
1311     $wheres = implode(" AND ", $wheres);
1313     return new \core\dml\sql_join($joins, $wheres, $params);
1316 /**
1317  * Returns array with sql code and parameters returning all ids
1318  * of users enrolled into course.
1319  *
1320  * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1321  *
1322  * @param context $context
1323  * @param string $withcapability
1324  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1325  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1326  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1327  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1328  * @return array list($sql, $params)
1329  */
1330 function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false,
1331                           $enrolid = 0) {
1333     // Use unique prefix just in case somebody makes some SQL magic with the result.
1334     static $i = 0;
1335     $i++;
1336     $prefix = 'eu' . $i . '_';
1338     $capjoin = get_enrolled_with_capabilities_join(
1339             $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended, $enrolid);
1341     $sql = "SELECT DISTINCT {$prefix}u.id
1342               FROM {user} {$prefix}u
1343             $capjoin->joins
1344              WHERE $capjoin->wheres";
1346     return array($sql, $capjoin->params);
1349 /**
1350  * Returns array with sql joins and parameters returning all ids
1351  * of users enrolled into course.
1352  *
1353  * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1354  *
1355  * @throws coding_exception
1356  *
1357  * @param context $context
1358  * @param string $useridcolumn User id column used the calling query, e.g. u.id
1359  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1360  * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1361  * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1362  * @return \core\dml\sql_join Contains joins, wheres, params
1363  */
1364 function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1365     // Use unique prefix just in case somebody makes some SQL magic with the result.
1366     static $i = 0;
1367     $i++;
1368     $prefix = 'ej' . $i . '_';
1370     // First find the course context.
1371     $coursecontext = $context->get_course_context();
1373     $isfrontpage = ($coursecontext->instanceid == SITEID);
1375     if ($onlyactive && $onlysuspended) {
1376         throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1377     }
1378     if ($isfrontpage && $onlysuspended) {
1379         throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1380     }
1382     $joins  = array();
1383     $wheres = array();
1384     $params = array();
1386     $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1388     // Note all users are "enrolled" on the frontpage, but for others...
1389     if (!$isfrontpage) {
1390         $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1391         $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1393         $enrolconditions = array(
1394             "{$prefix}e.id = {$prefix}ue.enrolid",
1395             "{$prefix}e.courseid = :{$prefix}courseid",
1396         );
1397         if ($enrolid) {
1398             $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1399             $params[$prefix . 'enrolid'] = $enrolid;
1400         }
1401         $enrolconditionssql = implode(" AND ", $enrolconditions);
1402         $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1404         $params[$prefix.'courseid'] = $coursecontext->instanceid;
1406         if (!$onlysuspended) {
1407             $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1408             $joins[] = $ejoin;
1409             if ($onlyactive) {
1410                 $wheres[] = "$where1 AND $where2";
1411             }
1412         } else {
1413             // Suspended only where there is enrolment but ALL are suspended.
1414             // Consider multiple enrols where one is not suspended or plain role_assign.
1415             $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1416             $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1417             $enrolconditions = array(
1418                 "{$prefix}e1.id = {$prefix}ue1.enrolid",
1419                 "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1420             );
1421             if ($enrolid) {
1422                 $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1.enrolid";
1423                 $params[$prefix . 'e1.enrolid'] = $enrolid;
1424             }
1425             $enrolconditionssql = implode(" AND ", $enrolconditions);
1426             $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1427             $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1428             $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1429         }
1431         if ($onlyactive || $onlysuspended) {
1432             $now = round(time(), -2); // Rounding helps caching in DB.
1433             $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1434                     $prefix . 'active' => ENROL_USER_ACTIVE,
1435                     $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1436         }
1437     }
1439     $joins = implode("\n", $joins);
1440     $wheres = implode(" AND ", $wheres);
1442     return new \core\dml\sql_join($joins, $wheres, $params);
1445 /**
1446  * Returns list of users enrolled into course.
1447  *
1448  * @param context $context
1449  * @param string $withcapability
1450  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1451  * @param string $userfields requested user record fields
1452  * @param string $orderby
1453  * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1454  * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1455  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1456  * @return array of user records
1457  */
1458 function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
1459         $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1460     global $DB;
1462     list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
1463     $sql = "SELECT $userfields
1464               FROM {user} u
1465               JOIN ($esql) je ON je.id = u.id
1466              WHERE u.deleted = 0";
1468     if ($orderby) {
1469         $sql = "$sql ORDER BY $orderby";
1470     } else {
1471         list($sort, $sortparams) = users_order_by_sql('u');
1472         $sql = "$sql ORDER BY $sort";
1473         $params = array_merge($params, $sortparams);
1474     }
1476     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1479 /**
1480  * Counts list of users enrolled into course (as per above function)
1481  *
1482  * @param context $context
1483  * @param string $withcapability
1484  * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1485  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1486  * @return array of user records
1487  */
1488 function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
1489     global $DB;
1491     $capjoin = get_enrolled_with_capabilities_join(
1492             $context, '', $withcapability, $groupid, $onlyactive);
1494     $sql = "SELECT count(u.id)
1495               FROM {user} u
1496             $capjoin->joins
1497              WHERE $capjoin->wheres AND u.deleted = 0";
1499     return $DB->count_records_sql($sql, $capjoin->params);
1502 /**
1503  * Send welcome email "from" options.
1504  *
1505  * @return array list of from options
1506  */
1507 function enrol_send_welcome_email_options() {
1508     return [
1509         ENROL_DO_NOT_SEND_EMAIL                 => get_string('no'),
1510         ENROL_SEND_EMAIL_FROM_COURSE_CONTACT    => get_string('sendfromcoursecontact', 'enrol'),
1511         ENROL_SEND_EMAIL_FROM_KEY_HOLDER        => get_string('sendfromkeyholder', 'enrol'),
1512         ENROL_SEND_EMAIL_FROM_NOREPLY           => get_string('sendfromnoreply', 'enrol')
1513     ];
1516 /**
1517  * Serve the user enrolment form as a fragment.
1518  *
1519  * @param array $args List of named arguments for the fragment loader.
1520  * @return string
1521  */
1522 function enrol_output_fragment_user_enrolment_form($args) {
1523     global $CFG, $DB;
1525     $args = (object) $args;
1526     $context = $args->context;
1527     require_capability('moodle/course:enrolreview', $context);
1529     $ueid = $args->ueid;
1530     $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1531     $customdata = [
1532         'ue' => $userenrolment,
1533         'modal' => true,
1534     ];
1536     // Set the data if applicable.
1537     $data = [];
1538     if (isset($args->formdata)) {
1539         $serialiseddata = json_decode($args->formdata);
1540         parse_str($serialiseddata, $data);
1541     }
1543     require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1544     $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1546     if (!empty($data)) {
1547         $mform->set_data($data);
1548         $mform->is_validated();
1549     }
1551     return $mform->render();
1554 /**
1555  * Returns the course where a user enrolment belong to.
1556  *
1557  * @param int $ueid user_enrolments id
1558  * @return stdClass
1559  */
1560 function enrol_get_course_by_user_enrolment_id($ueid) {
1561     global $DB;
1562     $sql = "SELECT c.* FROM {user_enrolments} ue
1563               JOIN {enrol} e ON e.id = ue.enrolid
1564               JOIN {course} c ON c.id = e.courseid
1565              WHERE ue.id = :ueid";
1566     return $DB->get_record_sql($sql, array('ueid' => $ueid));
1569 /**
1570  * Return all users enrolled in a course.
1571  *
1572  * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1573  * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1574  * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1575  * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1576  * @return stdClass[]
1577  */
1578 function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
1579     global $DB;
1581     if (!$courseid && !$usersfilter && !$uefilter) {
1582         throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1583     }
1585     $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1586              ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1587              ue.timemodified AS uetimemodified,
1588              u.* FROM {user_enrolments} ue
1589               JOIN {enrol} e ON e.id = ue.enrolid
1590               JOIN {user} u ON ue.userid = u.id
1591              WHERE ";
1592     $params = array();
1594     if ($courseid) {
1595         $conditions[] = "e.courseid = :courseid";
1596         $params['courseid'] = $courseid;
1597     }
1599     if ($onlyactive) {
1600         $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1601             "(ue.timeend = 0 OR ue.timeend > :now2)";
1602         // Improves db caching.
1603         $params['now1']    = round(time(), -2);
1604         $params['now2']    = $params['now1'];
1605         $params['active']  = ENROL_USER_ACTIVE;
1606         $params['enabled'] = ENROL_INSTANCE_ENABLED;
1607     }
1609     if ($usersfilter) {
1610         list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1611         $conditions[] = "ue.userid $usersql";
1612         $params = $params + $userparams;
1613     }
1615     if ($uefilter) {
1616         list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1617         $conditions[] = "ue.id $uesql";
1618         $params = $params + $ueparams;
1619     }
1621     return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1624 /**
1625  * Enrolment plugins abstract class.
1626  *
1627  * All enrol plugins should be based on this class,
1628  * this is also the main source of documentation.
1629  *
1630  * @copyright  2010 Petr Skoda {@link http://skodak.org}
1631  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1632  */
1633 abstract class enrol_plugin {
1634     protected $config = null;
1636     /**
1637      * Returns name of this enrol plugin
1638      * @return string
1639      */
1640     public function get_name() {
1641         // second word in class is always enrol name, sorry, no fancy plugin names with _
1642         $words = explode('_', get_class($this));
1643         return $words[1];
1644     }
1646     /**
1647      * Returns localised name of enrol instance
1648      *
1649      * @param object $instance (null is accepted too)
1650      * @return string
1651      */
1652     public function get_instance_name($instance) {
1653         if (empty($instance->name)) {
1654             $enrol = $this->get_name();
1655             return get_string('pluginname', 'enrol_'.$enrol);
1656         } else {
1657             $context = context_course::instance($instance->courseid);
1658             return format_string($instance->name, true, array('context'=>$context));
1659         }
1660     }
1662     /**
1663      * Returns optional enrolment information icons.
1664      *
1665      * This is used in course list for quick overview of enrolment options.
1666      *
1667      * We are not using single instance parameter because sometimes
1668      * we might want to prevent icon repetition when multiple instances
1669      * of one type exist. One instance may also produce several icons.
1670      *
1671      * @param array $instances all enrol instances of this type in one course
1672      * @return array of pix_icon
1673      */
1674     public function get_info_icons(array $instances) {
1675         return array();
1676     }
1678     /**
1679      * Returns optional enrolment instance description text.
1680      *
1681      * This is used in detailed course information.
1682      *
1683      *
1684      * @param object $instance
1685      * @return string short html text
1686      */
1687     public function get_description_text($instance) {
1688         return null;
1689     }
1691     /**
1692      * Makes sure config is loaded and cached.
1693      * @return void
1694      */
1695     protected function load_config() {
1696         if (!isset($this->config)) {
1697             $name = $this->get_name();
1698             $this->config = get_config("enrol_$name");
1699         }
1700     }
1702     /**
1703      * Returns plugin config value
1704      * @param  string $name
1705      * @param  string $default value if config does not exist yet
1706      * @return string value or default
1707      */
1708     public function get_config($name, $default = NULL) {
1709         $this->load_config();
1710         return isset($this->config->$name) ? $this->config->$name : $default;
1711     }
1713     /**
1714      * Sets plugin config value
1715      * @param  string $name name of config
1716      * @param  string $value string config value, null means delete
1717      * @return string value
1718      */
1719     public function set_config($name, $value) {
1720         $pluginname = $this->get_name();
1721         $this->load_config();
1722         if ($value === NULL) {
1723             unset($this->config->$name);
1724         } else {
1725             $this->config->$name = $value;
1726         }
1727         set_config($name, $value, "enrol_$pluginname");
1728     }
1730     /**
1731      * Does this plugin assign protected roles are can they be manually removed?
1732      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1733      */
1734     public function roles_protected() {
1735         return true;
1736     }
1738     /**
1739      * Does this plugin allow manual enrolments?
1740      *
1741      * @param stdClass $instance course enrol instance
1742      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1743      *
1744      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1745      */
1746     public function allow_enrol(stdClass $instance) {
1747         return false;
1748     }
1750     /**
1751      * Does this plugin allow manual unenrolment of all users?
1752      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1753      *
1754      * @param stdClass $instance course enrol instance
1755      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1756      */
1757     public function allow_unenrol(stdClass $instance) {
1758         return false;
1759     }
1761     /**
1762      * Does this plugin allow manual unenrolment of a specific user?
1763      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1764      *
1765      * This is useful especially for synchronisation plugins that
1766      * do suspend instead of full unenrolment.
1767      *
1768      * @param stdClass $instance course enrol instance
1769      * @param stdClass $ue record from user_enrolments table, specifies user
1770      *
1771      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1772      */
1773     public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1774         return $this->allow_unenrol($instance);
1775     }
1777     /**
1778      * Does this plugin allow manual changes in user_enrolments table?
1779      *
1780      * All plugins allowing this must implement 'enrol/xxx:manage' capability
1781      *
1782      * @param stdClass $instance course enrol instance
1783      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1784      */
1785     public function allow_manage(stdClass $instance) {
1786         return false;
1787     }
1789     /**
1790      * Does this plugin support some way to user to self enrol?
1791      *
1792      * @param stdClass $instance course enrol instance
1793      *
1794      * @return bool - true means show "Enrol me in this course" link in course UI
1795      */
1796     public function show_enrolme_link(stdClass $instance) {
1797         return false;
1798     }
1800     /**
1801      * Attempt to automatically enrol current user in course without any interaction,
1802      * calling code has to make sure the plugin and instance are active.
1803      *
1804      * This should return either a timestamp in the future or false.
1805      *
1806      * @param stdClass $instance course enrol instance
1807      * @return bool|int false means not enrolled, integer means timeend
1808      */
1809     public function try_autoenrol(stdClass $instance) {
1810         global $USER;
1812         return false;
1813     }
1815     /**
1816      * Attempt to automatically gain temporary guest access to course,
1817      * calling code has to make sure the plugin and instance are active.
1818      *
1819      * This should return either a timestamp in the future or false.
1820      *
1821      * @param stdClass $instance course enrol instance
1822      * @return bool|int false means no guest access, integer means timeend
1823      */
1824     public function try_guestaccess(stdClass $instance) {
1825         global $USER;
1827         return false;
1828     }
1830     /**
1831      * Enrol user into course via enrol instance.
1832      *
1833      * @param stdClass $instance
1834      * @param int $userid
1835      * @param int $roleid optional role id
1836      * @param int $timestart 0 means unknown
1837      * @param int $timeend 0 means forever
1838      * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
1839      * @param bool $recovergrades restore grade history
1840      * @return void
1841      */
1842     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
1843         global $DB, $USER, $CFG; // CFG necessary!!!
1845         if ($instance->courseid == SITEID) {
1846             throw new coding_exception('invalid attempt to enrol into frontpage course!');
1847         }
1849         $name = $this->get_name();
1850         $courseid = $instance->courseid;
1852         if ($instance->enrol !== $name) {
1853             throw new coding_exception('invalid enrol instance!');
1854         }
1855         $context = context_course::instance($instance->courseid, MUST_EXIST);
1856         if (!isset($recovergrades)) {
1857             $recovergrades = $CFG->recovergradesdefault;
1858         }
1860         $inserted = false;
1861         $updated  = false;
1862         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1863             //only update if timestart or timeend or status are different.
1864             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
1865                 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
1866             }
1867         } else {
1868             $ue = new stdClass();
1869             $ue->enrolid      = $instance->id;
1870             $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
1871             $ue->userid       = $userid;
1872             $ue->timestart    = $timestart;
1873             $ue->timeend      = $timeend;
1874             $ue->modifierid   = $USER->id;
1875             $ue->timecreated  = time();
1876             $ue->timemodified = $ue->timecreated;
1877             $ue->id = $DB->insert_record('user_enrolments', $ue);
1879             $inserted = true;
1880         }
1882         if ($inserted) {
1883             // Trigger event.
1884             $event = \core\event\user_enrolment_created::create(
1885                     array(
1886                         'objectid' => $ue->id,
1887                         'courseid' => $courseid,
1888                         'context' => $context,
1889                         'relateduserid' => $ue->userid,
1890                         'other' => array('enrol' => $name)
1891                         )
1892                     );
1893             $event->trigger();
1894             // Check if course contacts cache needs to be cleared.
1895             require_once($CFG->libdir . '/coursecatlib.php');
1896             coursecat::user_enrolment_changed($courseid, $ue->userid,
1897                     $ue->status, $ue->timestart, $ue->timeend);
1898         }
1900         if ($roleid) {
1901             // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
1902             if ($this->roles_protected()) {
1903                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1904             } else {
1905                 role_assign($roleid, $userid, $context->id);
1906             }
1907         }
1909         // Recover old grades if present.
1910         if ($recovergrades) {
1911             require_once("$CFG->libdir/gradelib.php");
1912             grade_recover_history_grades($userid, $courseid);
1913         }
1915         // reset current user enrolment caching
1916         if ($userid == $USER->id) {
1917             if (isset($USER->enrol['enrolled'][$courseid])) {
1918                 unset($USER->enrol['enrolled'][$courseid]);
1919             }
1920             if (isset($USER->enrol['tempguest'][$courseid])) {
1921                 unset($USER->enrol['tempguest'][$courseid]);
1922                 remove_temp_course_roles($context);
1923             }
1924         }
1925     }
1927     /**
1928      * Store user_enrolments changes and trigger event.
1929      *
1930      * @param stdClass $instance
1931      * @param int $userid
1932      * @param int $status
1933      * @param int $timestart
1934      * @param int $timeend
1935      * @return void
1936      */
1937     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1938         global $DB, $USER, $CFG;
1940         $name = $this->get_name();
1942         if ($instance->enrol !== $name) {
1943             throw new coding_exception('invalid enrol instance!');
1944         }
1946         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1947             // weird, user not enrolled
1948             return;
1949         }
1951         $modified = false;
1952         if (isset($status) and $ue->status != $status) {
1953             $ue->status = $status;
1954             $modified = true;
1955         }
1956         if (isset($timestart) and $ue->timestart != $timestart) {
1957             $ue->timestart = $timestart;
1958             $modified = true;
1959         }
1960         if (isset($timeend) and $ue->timeend != $timeend) {
1961             $ue->timeend = $timeend;
1962             $modified = true;
1963         }
1965         if (!$modified) {
1966             // no change
1967             return;
1968         }
1970         $ue->modifierid = $USER->id;
1971         $ue->timemodified = time();
1972         $DB->update_record('user_enrolments', $ue);
1973         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
1975         // Invalidate core_access cache for get_suspended_userids.
1976         cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
1978         // Trigger event.
1979         $event = \core\event\user_enrolment_updated::create(
1980                 array(
1981                     'objectid' => $ue->id,
1982                     'courseid' => $instance->courseid,
1983                     'context' => context_course::instance($instance->courseid),
1984                     'relateduserid' => $ue->userid,
1985                     'other' => array('enrol' => $name)
1986                     )
1987                 );
1988         $event->trigger();
1990         require_once($CFG->libdir . '/coursecatlib.php');
1991         coursecat::user_enrolment_changed($instance->courseid, $ue->userid,
1992                 $ue->status, $ue->timestart, $ue->timeend);
1993     }
1995     /**
1996      * Unenrol user from course,
1997      * the last unenrolment removes all remaining roles.
1998      *
1999      * @param stdClass $instance
2000      * @param int $userid
2001      * @return void
2002      */
2003     public function unenrol_user(stdClass $instance, $userid) {
2004         global $CFG, $USER, $DB;
2005         require_once("$CFG->dirroot/group/lib.php");
2007         $name = $this->get_name();
2008         $courseid = $instance->courseid;
2010         if ($instance->enrol !== $name) {
2011             throw new coding_exception('invalid enrol instance!');
2012         }
2013         $context = context_course::instance($instance->courseid, MUST_EXIST);
2015         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2016             // weird, user not enrolled
2017             return;
2018         }
2020         // Remove all users groups linked to this enrolment instance.
2021         if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2022             foreach ($gms as $gm) {
2023                 groups_remove_member($gm->groupid, $gm->userid);
2024             }
2025         }
2027         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2028         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2030         // add extra info and trigger event
2031         $ue->courseid  = $courseid;
2032         $ue->enrol     = $name;
2034         $sql = "SELECT 'x'
2035                   FROM {user_enrolments} ue
2036                   JOIN {enrol} e ON (e.id = ue.enrolid)
2037                  WHERE ue.userid = :userid AND e.courseid = :courseid";
2038         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2039             $ue->lastenrol = false;
2041         } else {
2042             // the big cleanup IS necessary!
2043             require_once("$CFG->libdir/gradelib.php");
2045             // remove all remaining roles
2046             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2048             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2049             groups_delete_group_members($courseid, $userid);
2051             grade_user_unenrol($courseid, $userid);
2053             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2055             $ue->lastenrol = true; // means user not enrolled any more
2056         }
2057         // Trigger event.
2058         $event = \core\event\user_enrolment_deleted::create(
2059                 array(
2060                     'courseid' => $courseid,
2061                     'context' => $context,
2062                     'relateduserid' => $ue->userid,
2063                     'objectid' => $ue->id,
2064                     'other' => array(
2065                         'userenrolment' => (array)$ue,
2066                         'enrol' => $name
2067                         )
2068                     )
2069                 );
2070         $event->trigger();
2071         // reset all enrol caches
2072         $context->mark_dirty();
2074         // Check if courrse contacts cache needs to be cleared.
2075         require_once($CFG->libdir . '/coursecatlib.php');
2076         coursecat::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2078         // reset current user enrolment caching
2079         if ($userid == $USER->id) {
2080             if (isset($USER->enrol['enrolled'][$courseid])) {
2081                 unset($USER->enrol['enrolled'][$courseid]);
2082             }
2083             if (isset($USER->enrol['tempguest'][$courseid])) {
2084                 unset($USER->enrol['tempguest'][$courseid]);
2085                 remove_temp_course_roles($context);
2086             }
2087         }
2088     }
2090     /**
2091      * Forces synchronisation of user enrolments.
2092      *
2093      * This is important especially for external enrol plugins,
2094      * this function is called for all enabled enrol plugins
2095      * right after every user login.
2096      *
2097      * @param object $user user record
2098      * @return void
2099      */
2100     public function sync_user_enrolments($user) {
2101         // override if necessary
2102     }
2104     /**
2105      * This returns false for backwards compatibility, but it is really recommended.
2106      *
2107      * @since Moodle 3.1
2108      * @return boolean
2109      */
2110     public function use_standard_editing_ui() {
2111         return false;
2112     }
2114     /**
2115      * Return whether or not, given the current state, it is possible to add a new instance
2116      * of this enrolment plugin to the course.
2117      *
2118      * Default implementation is just for backwards compatibility.
2119      *
2120      * @param int $courseid
2121      * @return boolean
2122      */
2123     public function can_add_instance($courseid) {
2124         $link = $this->get_newinstance_link($courseid);
2125         return !empty($link);
2126     }
2128     /**
2129      * Return whether or not, given the current state, it is possible to edit an instance
2130      * of this enrolment plugin in the course. Used by the standard editing UI
2131      * to generate a link to the edit instance form if editing is allowed.
2132      *
2133      * @param stdClass $instance
2134      * @return boolean
2135      */
2136     public function can_edit_instance($instance) {
2137         $context = context_course::instance($instance->courseid);
2139         return has_capability('enrol/' . $instance->enrol . ':config', $context);
2140     }
2142     /**
2143      * Returns link to page which may be used to add new instance of enrolment plugin in course.
2144      * @param int $courseid
2145      * @return moodle_url page url
2146      */
2147     public function get_newinstance_link($courseid) {
2148         // override for most plugins, check if instance already exists in cases only one instance is supported
2149         return NULL;
2150     }
2152     /**
2153      * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2154      */
2155     public function instance_deleteable($instance) {
2156         throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2157                 enrol_plugin::can_delete_instance() instead');
2158     }
2160     /**
2161      * Is it possible to delete enrol instance via standard UI?
2162      *
2163      * @param stdClass  $instance
2164      * @return bool
2165      */
2166     public function can_delete_instance($instance) {
2167         return false;
2168     }
2170     /**
2171      * Is it possible to hide/show enrol instance via standard UI?
2172      *
2173      * @param stdClass $instance
2174      * @return bool
2175      */
2176     public function can_hide_show_instance($instance) {
2177         debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2178         return true;
2179     }
2181     /**
2182      * Returns link to manual enrol UI if exists.
2183      * Does the access control tests automatically.
2184      *
2185      * @param object $instance
2186      * @return moodle_url
2187      */
2188     public function get_manual_enrol_link($instance) {
2189         return NULL;
2190     }
2192     /**
2193      * Returns list of unenrol links for all enrol instances in course.
2194      *
2195      * @param int $instance
2196      * @return moodle_url or NULL if self unenrolment not supported
2197      */
2198     public function get_unenrolself_link($instance) {
2199         global $USER, $CFG, $DB;
2201         $name = $this->get_name();
2202         if ($instance->enrol !== $name) {
2203             throw new coding_exception('invalid enrol instance!');
2204         }
2206         if ($instance->courseid == SITEID) {
2207             return NULL;
2208         }
2210         if (!enrol_is_enabled($name)) {
2211             return NULL;
2212         }
2214         if ($instance->status != ENROL_INSTANCE_ENABLED) {
2215             return NULL;
2216         }
2218         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2219             return NULL;
2220         }
2222         $context = context_course::instance($instance->courseid, MUST_EXIST);
2224         if (!has_capability("enrol/$name:unenrolself", $context)) {
2225             return NULL;
2226         }
2228         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2229             return NULL;
2230         }
2232         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2233     }
2235     /**
2236      * Adds enrol instance UI to course edit form
2237      *
2238      * @param object $instance enrol instance or null if does not exist yet
2239      * @param MoodleQuickForm $mform
2240      * @param object $data
2241      * @param object $context context of existing course or parent category if course does not exist
2242      * @return void
2243      */
2244     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2245         // override - usually at least enable/disable switch, has to add own form header
2246     }
2248     /**
2249      * Adds form elements to add/edit instance form.
2250      *
2251      * @since Moodle 3.1
2252      * @param object $instance enrol instance or null if does not exist yet
2253      * @param MoodleQuickForm $mform
2254      * @param context $context
2255      * @return void
2256      */
2257     public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2258         // Do nothing by default.
2259     }
2261     /**
2262      * Perform custom validation of the data used to edit the instance.
2263      *
2264      * @since Moodle 3.1
2265      * @param array $data array of ("fieldname"=>value) of submitted data
2266      * @param array $files array of uploaded files "element_name"=>tmp_file_path
2267      * @param object $instance The instance data loaded from the DB.
2268      * @param context $context The context of the instance we are editing
2269      * @return array of "element_name"=>"error_description" if there are errors,
2270      *         or an empty array if everything is OK.
2271      */
2272     public function edit_instance_validation($data, $files, $instance, $context) {
2273         // No errors by default.
2274         debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2275         return array();
2276     }
2278     /**
2279      * Validates course edit form data
2280      *
2281      * @param object $instance enrol instance or null if does not exist yet
2282      * @param array $data
2283      * @param object $context context of existing course or parent category if course does not exist
2284      * @return array errors array
2285      */
2286     public function course_edit_validation($instance, array $data, $context) {
2287         return array();
2288     }
2290     /**
2291      * Called after updating/inserting course.
2292      *
2293      * @param bool $inserted true if course just inserted
2294      * @param object $course
2295      * @param object $data form data
2296      * @return void
2297      */
2298     public function course_updated($inserted, $course, $data) {
2299         if ($inserted) {
2300             if ($this->get_config('defaultenrol')) {
2301                 $this->add_default_instance($course);
2302             }
2303         }
2304     }
2306     /**
2307      * Add new instance of enrol plugin.
2308      * @param object $course
2309      * @param array instance fields
2310      * @return int id of new instance, null if can not be created
2311      */
2312     public function add_instance($course, array $fields = NULL) {
2313         global $DB;
2315         if ($course->id == SITEID) {
2316             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2317         }
2319         $instance = new stdClass();
2320         $instance->enrol          = $this->get_name();
2321         $instance->status         = ENROL_INSTANCE_ENABLED;
2322         $instance->courseid       = $course->id;
2323         $instance->enrolstartdate = 0;
2324         $instance->enrolenddate   = 0;
2325         $instance->timemodified   = time();
2326         $instance->timecreated    = $instance->timemodified;
2327         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2329         $fields = (array)$fields;
2330         unset($fields['enrol']);
2331         unset($fields['courseid']);
2332         unset($fields['sortorder']);
2333         foreach($fields as $field=>$value) {
2334             $instance->$field = $value;
2335         }
2337         $instance->id = $DB->insert_record('enrol', $instance);
2339         \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2341         return $instance->id;
2342     }
2344     /**
2345      * Update instance of enrol plugin.
2346      *
2347      * @since Moodle 3.1
2348      * @param stdClass $instance
2349      * @param stdClass $data modified instance fields
2350      * @return boolean
2351      */
2352     public function update_instance($instance, $data) {
2353         global $DB;
2354         $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2355                             'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2356                             'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2357                             'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2358                             'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2359                             'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2361         foreach ($properties as $key) {
2362             if (isset($data->$key)) {
2363                 $instance->$key = $data->$key;
2364             }
2365         }
2366         $instance->timemodified = time();
2368         $update = $DB->update_record('enrol', $instance);
2369         if ($update) {
2370             \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2371         }
2372         return $update;
2373     }
2375     /**
2376      * Add new instance of enrol plugin with default settings,
2377      * called when adding new instance manually or when adding new course.
2378      *
2379      * Not all plugins support this.
2380      *
2381      * @param object $course
2382      * @return int id of new instance or null if no default supported
2383      */
2384     public function add_default_instance($course) {
2385         return null;
2386     }
2388     /**
2389      * Update instance status
2390      *
2391      * Override when plugin needs to do some action when enabled or disabled.
2392      *
2393      * @param stdClass $instance
2394      * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2395      * @return void
2396      */
2397     public function update_status($instance, $newstatus) {
2398         global $DB;
2400         $instance->status = $newstatus;
2401         $DB->update_record('enrol', $instance);
2403         $context = context_course::instance($instance->courseid);
2404         \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2406         // Invalidate all enrol caches.
2407         $context->mark_dirty();
2408     }
2410     /**
2411      * Delete course enrol plugin instance, unenrol all users.
2412      * @param object $instance
2413      * @return void
2414      */
2415     public function delete_instance($instance) {
2416         global $DB;
2418         $name = $this->get_name();
2419         if ($instance->enrol !== $name) {
2420             throw new coding_exception('invalid enrol instance!');
2421         }
2423         //first unenrol all users
2424         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2425         foreach ($participants as $participant) {
2426             $this->unenrol_user($instance, $participant->userid);
2427         }
2428         $participants->close();
2430         // now clean up all remainders that were not removed correctly
2431         $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2432         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2433         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2435         // finally drop the enrol row
2436         $DB->delete_records('enrol', array('id'=>$instance->id));
2438         $context = context_course::instance($instance->courseid);
2439         \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2441         // Invalidate all enrol caches.
2442         $context->mark_dirty();
2443     }
2445     /**
2446      * Creates course enrol form, checks if form submitted
2447      * and enrols user if necessary. It can also redirect.
2448      *
2449      * @param stdClass $instance
2450      * @return string html text, usually a form in a text box
2451      */
2452     public function enrol_page_hook(stdClass $instance) {
2453         return null;
2454     }
2456     /**
2457      * Checks if user can self enrol.
2458      *
2459      * @param stdClass $instance enrolment instance
2460      * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2461      *             used by navigation to improve performance.
2462      * @return bool|string true if successful, else error message or false
2463      */
2464     public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2465         return false;
2466     }
2468     /**
2469      * Return information for enrolment instance containing list of parameters required
2470      * for enrolment, name of enrolment plugin etc.
2471      *
2472      * @param stdClass $instance enrolment instance
2473      * @return array instance info.
2474      */
2475     public function get_enrol_info(stdClass $instance) {
2476         return null;
2477     }
2479     /**
2480      * Adds navigation links into course admin block.
2481      *
2482      * By defaults looks for manage links only.
2483      *
2484      * @param navigation_node $instancesnode
2485      * @param stdClass $instance
2486      * @return void
2487      */
2488     public function add_course_navigation($instancesnode, stdClass $instance) {
2489         if ($this->use_standard_editing_ui()) {
2490             $context = context_course::instance($instance->courseid);
2491             $cap = 'enrol/' . $instance->enrol . ':config';
2492             if (has_capability($cap, $context)) {
2493                 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2494                 $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2495                 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2496             }
2497         }
2498     }
2500     /**
2501      * Returns edit icons for the page with list of instances
2502      * @param stdClass $instance
2503      * @return array
2504      */
2505     public function get_action_icons(stdClass $instance) {
2506         global $OUTPUT;
2508         $icons = array();
2509         if ($this->use_standard_editing_ui()) {
2510             $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2511             $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2512             $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2513                 array('class' => 'iconsmall')));
2514         }
2515         return $icons;
2516     }
2518     /**
2519      * Reads version.php and determines if it is necessary
2520      * to execute the cron job now.
2521      * @return bool
2522      */
2523     public function is_cron_required() {
2524         global $CFG;
2526         $name = $this->get_name();
2527         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2528         $plugin = new stdClass();
2529         include($versionfile);
2530         if (empty($plugin->cron)) {
2531             return false;
2532         }
2533         $lastexecuted = $this->get_config('lastcron', 0);
2534         if ($lastexecuted + $plugin->cron < time()) {
2535             return true;
2536         } else {
2537             return false;
2538         }
2539     }
2541     /**
2542      * Called for all enabled enrol plugins that returned true from is_cron_required().
2543      * @return void
2544      */
2545     public function cron() {
2546     }
2548     /**
2549      * Called when user is about to be deleted
2550      * @param object $user
2551      * @return void
2552      */
2553     public function user_delete($user) {
2554         global $DB;
2556         $sql = "SELECT e.*
2557                   FROM {enrol} e
2558                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2559                  WHERE e.enrol = :name AND ue.userid = :userid";
2560         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2562         $rs = $DB->get_recordset_sql($sql, $params);
2563         foreach($rs as $instance) {
2564             $this->unenrol_user($instance, $user->id);
2565         }
2566         $rs->close();
2567     }
2569     /**
2570      * Returns an enrol_user_button that takes the user to a page where they are able to
2571      * enrol users into the managers course through this plugin.
2572      *
2573      * Optional: If the plugin supports manual enrolments it can choose to override this
2574      * otherwise it shouldn't
2575      *
2576      * @param course_enrolment_manager $manager
2577      * @return enrol_user_button|false
2578      */
2579     public function get_manual_enrol_button(course_enrolment_manager $manager) {
2580         return false;
2581     }
2583     /**
2584      * Gets an array of the user enrolment actions
2585      *
2586      * @param course_enrolment_manager $manager
2587      * @param stdClass $ue
2588      * @return array An array of user_enrolment_actions
2589      */
2590     public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2591         return array();
2592     }
2594     /**
2595      * Returns true if the plugin has one or more bulk operations that can be performed on
2596      * user enrolments.
2597      *
2598      * @param course_enrolment_manager $manager
2599      * @return bool
2600      */
2601     public function has_bulk_operations(course_enrolment_manager $manager) {
2602        return false;
2603     }
2605     /**
2606      * Return an array of enrol_bulk_enrolment_operation objects that define
2607      * the bulk actions that can be performed on user enrolments by the plugin.
2608      *
2609      * @param course_enrolment_manager $manager
2610      * @return array
2611      */
2612     public function get_bulk_operations(course_enrolment_manager $manager) {
2613         return array();
2614     }
2616     /**
2617      * Do any enrolments need expiration processing.
2618      *
2619      * Plugins that want to call this functionality must implement 'expiredaction' config setting.
2620      *
2621      * @param progress_trace $trace
2622      * @param int $courseid one course, empty mean all
2623      * @return bool true if any data processed, false if not
2624      */
2625     public function process_expirations(progress_trace $trace, $courseid = null) {
2626         global $DB;
2628         $name = $this->get_name();
2629         if (!enrol_is_enabled($name)) {
2630             $trace->finished();
2631             return false;
2632         }
2634         $processed = false;
2635         $params = array();
2636         $coursesql = "";
2637         if ($courseid) {
2638             $coursesql = "AND e.courseid = :courseid";
2639         }
2641         // Deal with expired accounts.
2642         $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
2644         if ($action == ENROL_EXT_REMOVED_UNENROL) {
2645             $instances = array();
2646             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2647                       FROM {user_enrolments} ue
2648                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2649                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2650                      WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
2651             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
2653             $rs = $DB->get_recordset_sql($sql, $params);
2654             foreach ($rs as $ue) {
2655                 if (!$processed) {
2656                     $trace->output("Starting processing of enrol_$name expirations...");
2657                     $processed = true;
2658                 }
2659                 if (empty($instances[$ue->enrolid])) {
2660                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2661                 }
2662                 $instance = $instances[$ue->enrolid];
2663                 if (!$this->roles_protected()) {
2664                     // Let's just guess what extra roles are supposed to be removed.
2665                     if ($instance->roleid) {
2666                         role_unassign($instance->roleid, $ue->userid, $ue->contextid);
2667                     }
2668                 }
2669                 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
2670                 $this->unenrol_user($instance, $ue->userid);
2671                 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
2672             }
2673             $rs->close();
2674             unset($instances);
2676         } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
2677             $instances = array();
2678             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2679                       FROM {user_enrolments} ue
2680                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2681                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2682                      WHERE ue.timeend > 0 AND ue.timeend < :now
2683                            AND ue.status = :useractive $coursesql";
2684             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
2685             $rs = $DB->get_recordset_sql($sql, $params);
2686             foreach ($rs as $ue) {
2687                 if (!$processed) {
2688                     $trace->output("Starting processing of enrol_$name expirations...");
2689                     $processed = true;
2690                 }
2691                 if (empty($instances[$ue->enrolid])) {
2692                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2693                 }
2694                 $instance = $instances[$ue->enrolid];
2696                 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
2697                     if (!$this->roles_protected()) {
2698                         // Let's just guess what roles should be removed.
2699                         $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
2700                         if ($count == 1) {
2701                             role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
2703                         } else if ($count > 1 and $instance->roleid) {
2704                             role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
2705                         }
2706                     }
2707                     // In any case remove all roles that belong to this instance and user.
2708                     role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
2709                     // Final cleanup of subcontexts if there are no more course roles.
2710                     if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
2711                         role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
2712                     }
2713                 }
2715                 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
2716                 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
2717             }
2718             $rs->close();
2719             unset($instances);
2721         } else {
2722             // ENROL_EXT_REMOVED_KEEP means no changes.
2723         }
2725         if ($processed) {
2726             $trace->output("...finished processing of enrol_$name expirations");
2727         } else {
2728             $trace->output("No expired enrol_$name enrolments detected");
2729         }
2730         $trace->finished();
2732         return $processed;
2733     }
2735     /**
2736      * Send expiry notifications.
2737      *
2738      * Plugin that wants to have expiry notification MUST implement following:
2739      * - expirynotifyhour plugin setting,
2740      * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
2741      * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
2742      *   expirymessageenrolledsubject and expirymessageenrolledbody),
2743      * - expiry_notification provider in db/messages.php,
2744      * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2745      * - something that calls this method, such as cron.
2746      *
2747      * @param progress_trace $trace (accepts bool for backwards compatibility only)
2748      */
2749     public function send_expiry_notifications($trace) {
2750         global $DB, $CFG;
2752         $name = $this->get_name();
2753         if (!enrol_is_enabled($name)) {
2754             $trace->finished();
2755             return;
2756         }
2758         // Unfortunately this may take a long time, it should not be interrupted,
2759         // otherwise users get duplicate notification.
2761         core_php_time_limit::raise();
2762         raise_memory_limit(MEMORY_HUGE);
2765         $expirynotifylast = $this->get_config('expirynotifylast', 0);
2766         $expirynotifyhour = $this->get_config('expirynotifyhour');
2767         if (is_null($expirynotifyhour)) {
2768             debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
2769             $trace->finished();
2770             return;
2771         }
2773         if (!($trace instanceof progress_trace)) {
2774             $trace = $trace ? new text_progress_trace() : new null_progress_trace();
2775             debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
2776         }
2778         $timenow = time();
2779         $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
2781         if ($expirynotifylast > $notifytime) {
2782             $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
2783             $trace->finished();
2784             return;
2786         } else if ($timenow < $notifytime) {
2787             $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
2788             $trace->finished();
2789             return;
2790         }
2792         $trace->output('Processing '.$name.' enrolment expiration notifications...');
2794         // Notify users responsible for enrolment once every day.
2795         $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
2796                   FROM {user_enrolments} ue
2797                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
2798                   JOIN {course} c ON (c.id = e.courseid)
2799                   JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
2800                  WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
2801               ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
2802         $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
2804         $rs = $DB->get_recordset_sql($sql, $params);
2806         $lastenrollid = 0;
2807         $users = array();
2809         foreach($rs as $ue) {
2810             if ($lastenrollid and $lastenrollid != $ue->enrolid) {
2811                 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2812                 $users = array();
2813             }
2814             $lastenrollid = $ue->enrolid;
2816             $enroller = $this->get_enroller($ue->enrolid);
2817             $context = context_course::instance($ue->courseid);
2819             $user = $DB->get_record('user', array('id'=>$ue->userid));
2821             $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
2823             if (!$ue->notifyall) {
2824                 continue;
2825             }
2827             if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
2828                 // Notify enrolled users only once at the start of the threshold.
2829                 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2830                 continue;
2831             }
2833             $this->notify_expiry_enrolled($user, $ue, $trace);
2834         }
2835         $rs->close();
2837         if ($lastenrollid and $users) {
2838             $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2839         }
2841         $trace->output('...notification processing finished.');
2842         $trace->finished();
2844         $this->set_config('expirynotifylast', $timenow);
2845     }
2847     /**
2848      * Returns the user who is responsible for enrolments for given instance.
2849      *
2850      * Override if plugin knows anybody better than admin.
2851      *
2852      * @param int $instanceid enrolment instance id
2853      * @return stdClass user record
2854      */
2855     protected function get_enroller($instanceid) {
2856         return get_admin();
2857     }
2859     /**
2860      * Notify user about incoming expiration of their enrolment,
2861      * it is called only if notification of enrolled users (aka students) is enabled in course.
2862      *
2863      * This is executed only once for each expiring enrolment right
2864      * at the start of the expiration threshold.
2865      *
2866      * @param stdClass $user
2867      * @param stdClass $ue
2868      * @param progress_trace $trace
2869      */
2870     protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
2871         global $CFG;
2873         $name = $this->get_name();
2875         $oldforcelang = force_current_language($user->lang);
2877         $enroller = $this->get_enroller($ue->enrolid);
2878         $context = context_course::instance($ue->courseid);
2880         $a = new stdClass();
2881         $a->course   = format_string($ue->fullname, true, array('context'=>$context));
2882         $a->user     = fullname($user, true);
2883         $a->timeend  = userdate($ue->timeend, '', $user->timezone);
2884         $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
2886         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
2887         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
2889         $message = new \core\message\message();
2890         $message->courseid          = $ue->courseid;
2891         $message->notification      = 1;
2892         $message->component         = 'enrol_'.$name;
2893         $message->name              = 'expiry_notification';
2894         $message->userfrom          = $enroller;
2895         $message->userto            = $user;
2896         $message->subject           = $subject;
2897         $message->fullmessage       = $body;
2898         $message->fullmessageformat = FORMAT_MARKDOWN;
2899         $message->fullmessagehtml   = markdown_to_html($body);
2900         $message->smallmessage      = $subject;
2901         $message->contexturlname    = $a->course;
2902         $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
2904         if (message_send($message)) {
2905             $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2906         } else {
2907             $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2908         }
2910         force_current_language($oldforcelang);
2911     }
2913     /**
2914      * Notify person responsible for enrolments that some user enrolments will be expired soon,
2915      * it is called only if notification of enrollers (aka teachers) is enabled in course.
2916      *
2917      * This is called repeatedly every day for each course if there are any pending expiration
2918      * in the expiration threshold.
2919      *
2920      * @param int $eid
2921      * @param array $users
2922      * @param progress_trace $trace
2923      */
2924     protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
2925         global $DB;
2927         $name = $this->get_name();
2929         $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
2930         $context = context_course::instance($instance->courseid);
2931         $course = $DB->get_record('course', array('id'=>$instance->courseid));
2933         $enroller = $this->get_enroller($instance->id);
2934         $admin = get_admin();
2936         $oldforcelang = force_current_language($enroller->lang);
2938         foreach($users as $key=>$info) {
2939             $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
2940         }
2942         $a = new stdClass();
2943         $a->course    = format_string($course->fullname, true, array('context'=>$context));
2944         $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
2945         $a->users     = implode("\n", $users);
2946         $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
2948         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
2949         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
2951         $message = new \core\message\message();
2952         $message->courseid          = $course->id;
2953         $message->notification      = 1;
2954         $message->component         = 'enrol_'.$name;
2955         $message->name              = 'expiry_notification';
2956         $message->userfrom          = $admin;
2957         $message->userto            = $enroller;
2958         $message->subject           = $subject;
2959         $message->fullmessage       = $body;
2960         $message->fullmessageformat = FORMAT_MARKDOWN;
2961         $message->fullmessagehtml   = markdown_to_html($body);
2962         $message->smallmessage      = $subject;
2963         $message->contexturlname    = $a->course;
2964         $message->contexturl        = $a->extendurl;
2966         if (message_send($message)) {
2967             $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2968         } else {
2969             $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2970         }
2972         force_current_language($oldforcelang);
2973     }
2975     /**
2976      * Backup execution step hook to annotate custom fields.
2977      *
2978      * @param backup_enrolments_execution_step $step
2979      * @param stdClass $enrol
2980      */
2981     public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
2982         // Override as necessary to annotate custom fields in the enrol table.
2983     }
2985     /**
2986      * Automatic enrol sync executed during restore.
2987      * Useful for automatic sync by course->idnumber or course category.
2988      * @param stdClass $course course record
2989      */
2990     public function restore_sync_course($course) {
2991         // Override if necessary.
2992     }
2994     /**
2995      * Restore instance and map settings.
2996      *
2997      * @param restore_enrolments_structure_step $step
2998      * @param stdClass $data
2999      * @param stdClass $course
3000      * @param int $oldid
3001      */
3002     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3003         // Do not call this from overridden methods, restore and set new id there.
3004         $step->set_mapping('enrol', $oldid, 0);
3005     }
3007     /**
3008      * Restore user enrolment.
3009      *
3010      * @param restore_enrolments_structure_step $step
3011      * @param stdClass $data
3012      * @param stdClass $instance
3013      * @param int $oldinstancestatus
3014      * @param int $userid
3015      */
3016     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3017         // Override as necessary if plugin supports restore of enrolments.
3018     }
3020     /**
3021      * Restore role assignment.
3022      *
3023      * @param stdClass $instance
3024      * @param int $roleid
3025      * @param int $userid
3026      * @param int $contextid
3027      */
3028     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3029         // No role assignment by default, override if necessary.
3030     }
3032     /**
3033      * Restore user group membership.
3034      * @param stdClass $instance
3035      * @param int $groupid
3036      * @param int $userid
3037      */
3038     public function restore_group_member($instance, $groupid, $userid) {
3039         // Implement if you want to restore protected group memberships,
3040         // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3041     }
3043     /**
3044      * Returns defaults for new instances.
3045      * @since Moodle 3.1
3046      * @return array
3047      */
3048     public function get_instance_defaults() {
3049         return array();
3050     }
3052     /**
3053      * Validate a list of parameter names and types.
3054      * @since Moodle 3.1
3055      *
3056      * @param array $data array of ("fieldname"=>value) of submitted data
3057      * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3058      * @return array of "element_name"=>"error_description" if there are errors,
3059      *         or an empty array if everything is OK.
3060      */
3061     public function validate_param_types($data, $rules) {
3062         $errors = array();
3063         $invalidstr = get_string('invaliddata', 'error');
3064         foreach ($rules as $fieldname => $rule) {
3065             if (is_array($rule)) {
3066                 if (!in_array($data[$fieldname], $rule)) {
3067                     $errors[$fieldname] = $invalidstr;
3068                 }
3069             } else {
3070                 if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3071                     $errors[$fieldname] = $invalidstr;
3072                 }
3073             }
3074         }
3075         return $errors;
3076     }