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