6295a108f07d525f635774d946fb43a7d4914e4f
[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  * Returns instances of enrol plugins
72  * @param bool $enabled return enabled only
73  * @return array of enrol plugins name=>instance
74  */
75 function enrol_get_plugins($enabled) {
76     global $CFG;
78     $result = array();
80     if ($enabled) {
81         // sorted by enabled plugin order
82         $enabled = explode(',', $CFG->enrol_plugins_enabled);
83         $plugins = array();
84         foreach ($enabled as $plugin) {
85             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
86         }
87     } else {
88         // sorted alphabetically
89         $plugins = get_plugin_list('enrol');
90         ksort($plugins);
91     }
93     foreach ($plugins as $plugin=>$location) {
94         if (!file_exists("$location/lib.php")) {
95             continue;
96         }
97         include_once("$location/lib.php");
98         $class = "enrol_{$plugin}_plugin";
99         if (!class_exists($class)) {
100             continue;
101         }
103         $result[$plugin] = new $class();
104     }
106     return $result;
109 /**
110  * Returns instance of enrol plugin
111  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
112  * @return enrol_plugin
113  */
114 function enrol_get_plugin($name) {
115     global $CFG;
117     $name = clean_param($name, PARAM_PLUGIN);
119     if (empty($name)) {
120         // ignore malformed or missing plugin names completely
121         return null;
122     }
124     $location = "$CFG->dirroot/enrol/$name";
126     if (!file_exists("$location/lib.php")) {
127         return null;
128     }
129     include_once("$location/lib.php");
130     $class = "enrol_{$name}_plugin";
131     if (!class_exists($class)) {
132         return null;
133     }
135     return new $class();
138 /**
139  * Returns enrolment instances in given course.
140  * @param int $courseid
141  * @param bool $enabled
142  * @return array of enrol instances
143  */
144 function enrol_get_instances($courseid, $enabled) {
145     global $DB, $CFG;
147     if (!$enabled) {
148         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
149     }
151     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
153     $enabled = explode(',', $CFG->enrol_plugins_enabled);
154     foreach ($result as $key=>$instance) {
155         if (!in_array($instance->enrol, $enabled)) {
156             unset($result[$key]);
157             continue;
158         }
159         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
160             // broken plugin
161             unset($result[$key]);
162             continue;
163         }
164     }
166     return $result;
169 /**
170  * Checks if a given plugin is in the list of enabled enrolment plugins.
171  *
172  * @param string $enrol Enrolment plugin name
173  * @return boolean Whether the plugin is enabled
174  */
175 function enrol_is_enabled($enrol) {
176     global $CFG;
178     if (empty($CFG->enrol_plugins_enabled)) {
179         return false;
180     }
181     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
184 /**
185  * Check all the login enrolment information for the given user object
186  * by querying the enrolment plugins
187  *
188  * This function may be very slow, use only once after log-in or login-as.
189  *
190  * @param stdClass $user
191  * @return void
192  */
193 function enrol_check_plugins($user) {
194     global $CFG;
196     if (empty($user->id) or isguestuser($user)) {
197         // shortcut - there is no enrolment work for guests and not-logged-in users
198         return;
199     }
201     // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
202     // which proved it was actually not necessary.
204     static $inprogress = array();  // To prevent this function being called more than once in an invocation
206     if (!empty($inprogress[$user->id])) {
207         return;
208     }
210     $inprogress[$user->id] = true;  // Set the flag
212     $enabled = enrol_get_plugins(true);
214     foreach($enabled as $enrol) {
215         $enrol->sync_user_enrolments($user);
216     }
218     unset($inprogress[$user->id]);  // Unset the flag
221 /**
222  * Do these two students share any course?
223  *
224  * The courses has to be visible and enrolments has to be active,
225  * timestart and timeend restrictions are ignored.
226  *
227  * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
228  * to true.
229  *
230  * @param stdClass|int $user1
231  * @param stdClass|int $user2
232  * @return bool
233  */
234 function enrol_sharing_course($user1, $user2) {
235     return enrol_get_shared_courses($user1, $user2, false, true);
238 /**
239  * Returns any courses shared by the two users
240  *
241  * The courses has to be visible and enrolments has to be active,
242  * timestart and timeend restrictions are ignored.
243  *
244  * @global moodle_database $DB
245  * @param stdClass|int $user1
246  * @param stdClass|int $user2
247  * @param bool $preloadcontexts If set to true contexts for the returned courses
248  *              will be preloaded.
249  * @param bool $checkexistsonly If set to true then this function will return true
250  *              if the users share any courses and false if not.
251  * @return array|bool An array of courses that both users are enrolled in OR if
252  *              $checkexistsonly set returns true if the users share any courses
253  *              and false if not.
254  */
255 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
256     global $DB, $CFG;
258     $user1 = isset($user1->id) ? $user1->id : $user1;
259     $user2 = isset($user2->id) ? $user2->id : $user2;
261     if (empty($user1) or empty($user2)) {
262         return false;
263     }
265     if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
266         return false;
267     }
269     list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee');
270     $params['enabled'] = ENROL_INSTANCE_ENABLED;
271     $params['active1'] = ENROL_USER_ACTIVE;
272     $params['active2'] = ENROL_USER_ACTIVE;
273     $params['user1']   = $user1;
274     $params['user2']   = $user2;
276     $ctxselect = '';
277     $ctxjoin = '';
278     if ($preloadcontexts) {
279         list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
280     }
282     $sql = "SELECT c.* $ctxselect
283               FROM {course} c
284               JOIN (
285                 SELECT DISTINCT c.id
286                   FROM {enrol} e
287                   JOIN {user_enrolments} ue1 ON (ue1.enrolid = e.id AND ue1.status = :active1 AND ue1.userid = :user1)
288                   JOIN {user_enrolments} ue2 ON (ue2.enrolid = e.id AND ue2.status = :active2 AND ue2.userid = :user2)
289                   JOIN {course} c ON (c.id = e.courseid AND c.visible = 1)
290                  WHERE e.status = :enabled AND e.enrol $plugins
291               ) ec ON ec.id = c.id
292               $ctxjoin";
294     if ($checkexistsonly) {
295         return $DB->record_exists_sql($sql, $params);
296     } else {
297         $courses = $DB->get_records_sql($sql, $params);
298         if ($preloadcontexts) {
299             array_map('context_instance_preload', $courses);
300         }
301         return $courses;
302     }
305 /**
306  * This function adds necessary enrol plugins UI into the course edit form.
307  *
308  * @param MoodleQuickForm $mform
309  * @param object $data course edit form data
310  * @param object $context context of existing course or parent category if course does not exist
311  * @return void
312  */
313 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
314     $plugins = enrol_get_plugins(true);
315     if (!empty($data->id)) {
316         $instances = enrol_get_instances($data->id, false);
317         foreach ($instances as $instance) {
318             if (!isset($plugins[$instance->enrol])) {
319                 continue;
320             }
321             $plugin = $plugins[$instance->enrol];
322             $plugin->course_edit_form($instance, $mform, $data, $context);
323         }
324     } else {
325         foreach ($plugins as $plugin) {
326             $plugin->course_edit_form(NULL, $mform, $data, $context);
327         }
328     }
331 /**
332  * Validate course edit form data
333  *
334  * @param array $data raw form data
335  * @param object $context context of existing course or parent category if course does not exist
336  * @return array errors array
337  */
338 function enrol_course_edit_validation(array $data, $context) {
339     $errors = array();
340     $plugins = enrol_get_plugins(true);
342     if (!empty($data['id'])) {
343         $instances = enrol_get_instances($data['id'], false);
344         foreach ($instances as $instance) {
345             if (!isset($plugins[$instance->enrol])) {
346                 continue;
347             }
348             $plugin = $plugins[$instance->enrol];
349             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
350         }
351     } else {
352         foreach ($plugins as $plugin) {
353             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
354         }
355     }
357     return $errors;
360 /**
361  * Update enrol instances after course edit form submission
362  * @param bool $inserted true means new course added, false course already existed
363  * @param object $course
364  * @param object $data form data
365  * @return void
366  */
367 function enrol_course_updated($inserted, $course, $data) {
368     global $DB, $CFG;
370     $plugins = enrol_get_plugins(true);
372     foreach ($plugins as $plugin) {
373         $plugin->course_updated($inserted, $course, $data);
374     }
377 /**
378  * Add navigation nodes
379  * @param navigation_node $coursenode
380  * @param object $course
381  * @return void
382  */
383 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
384     global $CFG;
386     $coursecontext = context_course::instance($course->id);
388     $instances = enrol_get_instances($course->id, true);
389     $plugins   = enrol_get_plugins(true);
391     // we do not want to break all course pages if there is some borked enrol plugin, right?
392     foreach ($instances as $k=>$instance) {
393         if (!isset($plugins[$instance->enrol])) {
394             unset($instances[$k]);
395         }
396     }
398     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
400     if ($course->id != SITEID) {
401         // list all participants - allows assigning roles, groups, etc.
402         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
403             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
404             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
405         }
407         // manage enrol plugin instances
408         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
409             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
410         } else {
411             $url = NULL;
412         }
413         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
415         // each instance decides how to configure itself or how many other nav items are exposed
416         foreach ($instances as $instance) {
417             if (!isset($plugins[$instance->enrol])) {
418                 continue;
419             }
420             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
421         }
423         if (!$url) {
424             $instancesnode->trim_if_empty();
425         }
426     }
428     // Manage groups in this course or even frontpage
429     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
430         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
431         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
432     }
434      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
435         // Override roles
436         if (has_capability('moodle/role:review', $coursecontext)) {
437             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
438         } else {
439             $url = NULL;
440         }
441         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
443         // Add assign or override roles if allowed
444         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
445             if (has_capability('moodle/role:assign', $coursecontext)) {
446                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
447                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
448             }
449         }
450         // Check role permissions
451         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
452             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
453             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
454         }
455      }
457      // Deal somehow with users that are not enrolled but still got a role somehow
458     if ($course->id != SITEID) {
459         //TODO, create some new UI for role assignments at course level
460         if (has_capability('moodle/role:assign', $coursecontext)) {
461             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
462             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
463         }
464     }
466     // just in case nothing was actually added
467     $usersnode->trim_if_empty();
469     if ($course->id != SITEID) {
470         if (isguestuser() or !isloggedin()) {
471             // guest account can not be enrolled - no links for them
472         } else if (is_enrolled($coursecontext)) {
473             // unenrol link if possible
474             foreach ($instances as $instance) {
475                 if (!isset($plugins[$instance->enrol])) {
476                     continue;
477                 }
478                 $plugin = $plugins[$instance->enrol];
479                 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
480                     $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
481                     $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
482                     break;
483                     //TODO. deal with multiple unenrol links - not likely case, but still...
484                 }
485             }
486         } else {
487             // enrol link if possible
488             if (is_viewing($coursecontext)) {
489                 // better not show any enrol link, this is intended for managers and inspectors
490             } else {
491                 foreach ($instances as $instance) {
492                     if (!isset($plugins[$instance->enrol])) {
493                         continue;
494                     }
495                     $plugin = $plugins[$instance->enrol];
496                     if ($plugin->show_enrolme_link($instance)) {
497                         $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
498                         $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
499                         $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
500                         break;
501                     }
502                 }
503             }
504         }
505     }
508 /**
509  * Returns list of courses current $USER is enrolled in and can access
510  *
511  * - $fields is an array of field names to ADD
512  *   so name the fields you really need, which will
513  *   be added and uniq'd
514  *
515  * @param string|array $fields
516  * @param string $sort
517  * @param int $limit max number of courses
518  * @return array
519  */
520 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
521     global $DB, $USER;
523     // Guest account does not have any courses
524     if (isguestuser() or !isloggedin()) {
525         return(array());
526     }
528     $basefields = array('id', 'category', 'sortorder',
529                         'shortname', 'fullname', 'idnumber',
530                         'startdate', 'visible',
531                         'groupmode', 'groupmodeforce');
533     if (empty($fields)) {
534         $fields = $basefields;
535     } else if (is_string($fields)) {
536         // turn the fields from a string to an array
537         $fields = explode(',', $fields);
538         $fields = array_map('trim', $fields);
539         $fields = array_unique(array_merge($basefields, $fields));
540     } else if (is_array($fields)) {
541         $fields = array_unique(array_merge($basefields, $fields));
542     } else {
543         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
544     }
545     if (in_array('*', $fields)) {
546         $fields = array('*');
547     }
549     $orderby = "";
550     $sort    = trim($sort);
551     if (!empty($sort)) {
552         $rawsorts = explode(',', $sort);
553         $sorts = array();
554         foreach ($rawsorts as $rawsort) {
555             $rawsort = trim($rawsort);
556             if (strpos($rawsort, 'c.') === 0) {
557                 $rawsort = substr($rawsort, 2);
558             }
559             $sorts[] = trim($rawsort);
560         }
561         $sort = 'c.'.implode(',c.', $sorts);
562         $orderby = "ORDER BY $sort";
563     }
565     $wheres = array("c.id <> :siteid");
566     $params = array('siteid'=>SITEID);
568     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
569         // list _only_ this course - anything else is asking for trouble...
570         $wheres[] = "courseid = :loginas";
571         $params['loginas'] = $USER->loginascontext->instanceid;
572     }
574     $coursefields = 'c.' .join(',c.', $fields);
575     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
576     $wheres = implode(" AND ", $wheres);
578     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
579     $sql = "SELECT $coursefields $ccselect
580               FROM {course} c
581               JOIN (SELECT DISTINCT e.courseid
582                       FROM {enrol} e
583                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
584                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
585                    ) en ON (en.courseid = c.id)
586            $ccjoin
587              WHERE $wheres
588           $orderby";
589     $params['userid']  = $USER->id;
590     $params['active']  = ENROL_USER_ACTIVE;
591     $params['enabled'] = ENROL_INSTANCE_ENABLED;
592     $params['now1']    = round(time(), -2); // improves db caching
593     $params['now2']    = $params['now1'];
595     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
597     // preload contexts and check visibility
598     foreach ($courses as $id=>$course) {
599         context_instance_preload($course);
600         if (!$course->visible) {
601             if (!$context = context_course::instance($id, IGNORE_MISSING)) {
602                 unset($courses[$id]);
603                 continue;
604             }
605             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
606                 unset($courses[$id]);
607                 continue;
608             }
609         }
610         $courses[$id] = $course;
611     }
613     //wow! Is that really all? :-D
615     return $courses;
618 /**
619  * Returns course enrolment information icons.
620  *
621  * @param object $course
622  * @param array $instances enrol instances of this course, improves performance
623  * @return array of pix_icon
624  */
625 function enrol_get_course_info_icons($course, array $instances = NULL) {
626     $icons = array();
627     if (is_null($instances)) {
628         $instances = enrol_get_instances($course->id, true);
629     }
630     $plugins = enrol_get_plugins(true);
631     foreach ($plugins as $name => $plugin) {
632         $pis = array();
633         foreach ($instances as $instance) {
634             if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
635                 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
636                 continue;
637             }
638             if ($instance->enrol == $name) {
639                 $pis[$instance->id] = $instance;
640             }
641         }
642         if ($pis) {
643             $icons = array_merge($icons, $plugin->get_info_icons($pis));
644         }
645     }
646     return $icons;
649 /**
650  * Returns course enrolment detailed information.
651  *
652  * @param object $course
653  * @return array of html fragments - can be used to construct lists
654  */
655 function enrol_get_course_description_texts($course) {
656     $lines = array();
657     $instances = enrol_get_instances($course->id, true);
658     $plugins = enrol_get_plugins(true);
659     foreach ($instances as $instance) {
660         if (!isset($plugins[$instance->enrol])) {
661             //weird
662             continue;
663         }
664         $plugin = $plugins[$instance->enrol];
665         $text = $plugin->get_description_text($instance);
666         if ($text !== NULL) {
667             $lines[] = $text;
668         }
669     }
670     return $lines;
673 /**
674  * Returns list of courses user is enrolled into.
675  * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
676  *
677  * - $fields is an array of fieldnames to ADD
678  *   so name the fields you really need, which will
679  *   be added and uniq'd
680  *
681  * @param int $userid
682  * @param bool $onlyactive return only active enrolments in courses user may see
683  * @param string|array $fields
684  * @param string $sort
685  * @return array
686  */
687 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
688     global $DB;
690     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
692     // preload contexts and check visibility
693     if ($onlyactive) {
694         foreach ($courses as $id=>$course) {
695             context_instance_preload($course);
696             if (!$course->visible) {
697                 if (!$context = context_course::instance($id)) {
698                     unset($courses[$id]);
699                     continue;
700                 }
701                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
702                     unset($courses[$id]);
703                     continue;
704                 }
705             }
706         }
707     }
709     return $courses;
713 /**
714  * Can user access at least one enrolled course?
715  *
716  * Cheat if necessary, but find out as fast as possible!
717  *
718  * @param int|stdClass $user null means use current user
719  * @return bool
720  */
721 function enrol_user_sees_own_courses($user = null) {
722     global $USER;
724     if ($user === null) {
725         $user = $USER;
726     }
727     $userid = is_object($user) ? $user->id : $user;
729     // Guest account does not have any courses
730     if (isguestuser($userid) or empty($userid)) {
731         return false;
732     }
734     // Let's cheat here if this is the current user,
735     // if user accessed any course recently, then most probably
736     // we do not need to query the database at all.
737     if ($USER->id == $userid) {
738         if (!empty($USER->enrol['enrolled'])) {
739             foreach ($USER->enrol['enrolled'] as $until) {
740                 if ($until > time()) {
741                     return true;
742                 }
743             }
744         }
745     }
747     // Now the slow way.
748     $courses = enrol_get_all_users_courses($userid, true);
749     foreach($courses as $course) {
750         if ($course->visible) {
751             return true;
752         }
753         context_helper::preload_from_record($course);
754         $context = context_course::instance($course->id);
755         if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
756             return true;
757         }
758     }
760     return false;
763 /**
764  * Returns list of courses user is enrolled into without any capability checks
765  * - $fields is an array of fieldnames to ADD
766  *   so name the fields you really need, which will
767  *   be added and uniq'd
768  *
769  * @param int $userid
770  * @param bool $onlyactive return only active enrolments in courses user may see
771  * @param string|array $fields
772  * @param string $sort
773  * @return array
774  */
775 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
776     global $DB;
778     // Guest account does not have any courses
779     if (isguestuser($userid) or empty($userid)) {
780         return(array());
781     }
783     $basefields = array('id', 'category', 'sortorder',
784             'shortname', 'fullname', 'idnumber',
785             'startdate', 'visible',
786             'groupmode', 'groupmodeforce');
788     if (empty($fields)) {
789         $fields = $basefields;
790     } else if (is_string($fields)) {
791         // turn the fields from a string to an array
792         $fields = explode(',', $fields);
793         $fields = array_map('trim', $fields);
794         $fields = array_unique(array_merge($basefields, $fields));
795     } else if (is_array($fields)) {
796         $fields = array_unique(array_merge($basefields, $fields));
797     } else {
798         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
799     }
800     if (in_array('*', $fields)) {
801         $fields = array('*');
802     }
804     $orderby = "";
805     $sort    = trim($sort);
806     if (!empty($sort)) {
807         $rawsorts = explode(',', $sort);
808         $sorts = array();
809         foreach ($rawsorts as $rawsort) {
810             $rawsort = trim($rawsort);
811             if (strpos($rawsort, 'c.') === 0) {
812                 $rawsort = substr($rawsort, 2);
813             }
814             $sorts[] = trim($rawsort);
815         }
816         $sort = 'c.'.implode(',c.', $sorts);
817         $orderby = "ORDER BY $sort";
818     }
820     $params = array('siteid'=>SITEID);
822     if ($onlyactive) {
823         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
824         $params['now1']    = round(time(), -2); // improves db caching
825         $params['now2']    = $params['now1'];
826         $params['active']  = ENROL_USER_ACTIVE;
827         $params['enabled'] = ENROL_INSTANCE_ENABLED;
828     } else {
829         $subwhere = "";
830     }
832     $coursefields = 'c.' .join(',c.', $fields);
833     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
835     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
836     $sql = "SELECT $coursefields $ccselect
837               FROM {course} c
838               JOIN (SELECT DISTINCT e.courseid
839                       FROM {enrol} e
840                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
841                  $subwhere
842                    ) en ON (en.courseid = c.id)
843            $ccjoin
844              WHERE c.id <> :siteid
845           $orderby";
846     $params['userid']  = $userid;
848     $courses = $DB->get_records_sql($sql, $params);
850     return $courses;
855 /**
856  * Called when user is about to be deleted.
857  * @param object $user
858  * @return void
859  */
860 function enrol_user_delete($user) {
861     global $DB;
863     $plugins = enrol_get_plugins(true);
864     foreach ($plugins as $plugin) {
865         $plugin->user_delete($user);
866     }
868     // force cleanup of all broken enrolments
869     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
872 /**
873  * Called when course is about to be deleted.
874  * @param stdClass $course
875  * @return void
876  */
877 function enrol_course_delete($course) {
878     global $DB;
880     $instances = enrol_get_instances($course->id, false);
881     $plugins = enrol_get_plugins(true);
882     foreach ($instances as $instance) {
883         if (isset($plugins[$instance->enrol])) {
884             $plugins[$instance->enrol]->delete_instance($instance);
885         }
886         // low level delete in case plugin did not do it
887         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
888         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
889         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
890         $DB->delete_records('enrol', array('id'=>$instance->id));
891     }
894 /**
895  * Try to enrol user via default internal auth plugin.
896  *
897  * For now this is always using the manual enrol plugin...
898  *
899  * @param $courseid
900  * @param $userid
901  * @param $roleid
902  * @param $timestart
903  * @param $timeend
904  * @return bool success
905  */
906 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
907     global $DB;
909     //note: this is hardcoded to manual plugin for now
911     if (!enrol_is_enabled('manual')) {
912         return false;
913     }
915     if (!$enrol = enrol_get_plugin('manual')) {
916         return false;
917     }
918     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
919         return false;
920     }
921     $instance = reset($instances);
923     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
925     return true;
928 /**
929  * Is there a chance users might self enrol
930  * @param int $courseid
931  * @return bool
932  */
933 function enrol_selfenrol_available($courseid) {
934     $result = false;
936     $plugins = enrol_get_plugins(true);
937     $enrolinstances = enrol_get_instances($courseid, true);
938     foreach($enrolinstances as $instance) {
939         if (!isset($plugins[$instance->enrol])) {
940             continue;
941         }
942         if ($instance->enrol === 'guest') {
943             // blacklist known temporary guest plugins
944             continue;
945         }
946         if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
947             $result = true;
948             break;
949         }
950     }
952     return $result;
955 /**
956  * This function returns the end of current active user enrolment.
957  *
958  * It deals correctly with multiple overlapping user enrolments.
959  *
960  * @param int $courseid
961  * @param int $userid
962  * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
963  */
964 function enrol_get_enrolment_end($courseid, $userid) {
965     global $DB;
967     $sql = "SELECT ue.*
968               FROM {user_enrolments} ue
969               JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
970               JOIN {user} u ON u.id = ue.userid
971              WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
972     $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
974     if (!$enrolments = $DB->get_records_sql($sql, $params)) {
975         return false;
976     }
978     $changes = array();
980     foreach ($enrolments as $ue) {
981         $start = (int)$ue->timestart;
982         $end = (int)$ue->timeend;
983         if ($end != 0 and $end < $start) {
984             debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
985             continue;
986         }
987         if (isset($changes[$start])) {
988             $changes[$start] = $changes[$start] + 1;
989         } else {
990             $changes[$start] = 1;
991         }
992         if ($end === 0) {
993             // no end
994         } else if (isset($changes[$end])) {
995             $changes[$end] = $changes[$end] - 1;
996         } else {
997             $changes[$end] = -1;
998         }
999     }
1001     // let's sort then enrolment starts&ends and go through them chronologically,
1002     // looking for current status and the next future end of enrolment
1003     ksort($changes);
1005     $now = time();
1006     $current = 0;
1007     $present = null;
1009     foreach ($changes as $time => $change) {
1010         if ($time > $now) {
1011             if ($present === null) {
1012                 // we have just went past current time
1013                 $present = $current;
1014                 if ($present < 1) {
1015                     // no enrolment active
1016                     return false;
1017                 }
1018             }
1019             if ($present !== null) {
1020                 // we are already in the future - look for possible end
1021                 if ($current + $change < 1) {
1022                     return $time;
1023                 }
1024             }
1025         }
1026         $current += $change;
1027     }
1029     if ($current > 0) {
1030         return 0;
1031     } else {
1032         return false;
1033     }
1037 /**
1038  * All enrol plugins should be based on this class,
1039  * this is also the main source of documentation.
1040  */
1041 abstract class enrol_plugin {
1042     protected $config = null;
1044     /**
1045      * Returns name of this enrol plugin
1046      * @return string
1047      */
1048     public function get_name() {
1049         // second word in class is always enrol name, sorry, no fancy plugin names with _
1050         $words = explode('_', get_class($this));
1051         return $words[1];
1052     }
1054     /**
1055      * Returns localised name of enrol instance
1056      *
1057      * @param object $instance (null is accepted too)
1058      * @return string
1059      */
1060     public function get_instance_name($instance) {
1061         if (empty($instance->name)) {
1062             $enrol = $this->get_name();
1063             return get_string('pluginname', 'enrol_'.$enrol);
1064         } else {
1065             $context = context_course::instance($instance->courseid);
1066             return format_string($instance->name, true, array('context'=>$context));
1067         }
1068     }
1070     /**
1071      * Returns optional enrolment information icons.
1072      *
1073      * This is used in course list for quick overview of enrolment options.
1074      *
1075      * We are not using single instance parameter because sometimes
1076      * we might want to prevent icon repetition when multiple instances
1077      * of one type exist. One instance may also produce several icons.
1078      *
1079      * @param array $instances all enrol instances of this type in one course
1080      * @return array of pix_icon
1081      */
1082     public function get_info_icons(array $instances) {
1083         return array();
1084     }
1086     /**
1087      * Returns optional enrolment instance description text.
1088      *
1089      * This is used in detailed course information.
1090      *
1091      *
1092      * @param object $instance
1093      * @return string short html text
1094      */
1095     public function get_description_text($instance) {
1096         return null;
1097     }
1099     /**
1100      * Makes sure config is loaded and cached.
1101      * @return void
1102      */
1103     protected function load_config() {
1104         if (!isset($this->config)) {
1105             $name = $this->get_name();
1106             $this->config = get_config("enrol_$name");
1107         }
1108     }
1110     /**
1111      * Returns plugin config value
1112      * @param  string $name
1113      * @param  string $default value if config does not exist yet
1114      * @return string value or default
1115      */
1116     public function get_config($name, $default = NULL) {
1117         $this->load_config();
1118         return isset($this->config->$name) ? $this->config->$name : $default;
1119     }
1121     /**
1122      * Sets plugin config value
1123      * @param  string $name name of config
1124      * @param  string $value string config value, null means delete
1125      * @return string value
1126      */
1127     public function set_config($name, $value) {
1128         $pluginname = $this->get_name();
1129         $this->load_config();
1130         if ($value === NULL) {
1131             unset($this->config->$name);
1132         } else {
1133             $this->config->$name = $value;
1134         }
1135         set_config($name, $value, "enrol_$pluginname");
1136     }
1138     /**
1139      * Does this plugin assign protected roles are can they be manually removed?
1140      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1141      */
1142     public function roles_protected() {
1143         return true;
1144     }
1146     /**
1147      * Does this plugin allow manual enrolments?
1148      *
1149      * @param stdClass $instance course enrol instance
1150      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1151      *
1152      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1153      */
1154     public function allow_enrol(stdClass $instance) {
1155         return false;
1156     }
1158     /**
1159      * Does this plugin allow manual unenrolment of all users?
1160      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1161      *
1162      * @param stdClass $instance course enrol instance
1163      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1164      */
1165     public function allow_unenrol(stdClass $instance) {
1166         return false;
1167     }
1169     /**
1170      * Does this plugin allow manual unenrolment of a specific user?
1171      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1172      *
1173      * This is useful especially for synchronisation plugins that
1174      * do suspend instead of full unenrolment.
1175      *
1176      * @param stdClass $instance course enrol instance
1177      * @param stdClass $ue record from user_enrolments table, specifies user
1178      *
1179      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1180      */
1181     public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1182         return $this->allow_unenrol($instance);
1183     }
1185     /**
1186      * Does this plugin allow manual changes in user_enrolments table?
1187      *
1188      * All plugins allowing this must implement 'enrol/xxx:manage' capability
1189      *
1190      * @param stdClass $instance course enrol instance
1191      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1192      */
1193     public function allow_manage(stdClass $instance) {
1194         return false;
1195     }
1197     /**
1198      * Does this plugin support some way to user to self enrol?
1199      *
1200      * @param stdClass $instance course enrol instance
1201      *
1202      * @return bool - true means show "Enrol me in this course" link in course UI
1203      */
1204     public function show_enrolme_link(stdClass $instance) {
1205         return false;
1206     }
1208     /**
1209      * Attempt to automatically enrol current user in course without any interaction,
1210      * calling code has to make sure the plugin and instance are active.
1211      *
1212      * This should return either a timestamp in the future or false.
1213      *
1214      * @param stdClass $instance course enrol instance
1215      * @return bool|int false means not enrolled, integer means timeend
1216      */
1217     public function try_autoenrol(stdClass $instance) {
1218         global $USER;
1220         return false;
1221     }
1223     /**
1224      * Attempt to automatically gain temporary guest access to course,
1225      * calling code has to make sure the plugin and instance are active.
1226      *
1227      * This should return either a timestamp in the future or false.
1228      *
1229      * @param stdClass $instance course enrol instance
1230      * @return bool|int false means no guest access, integer means timeend
1231      */
1232     public function try_guestaccess(stdClass $instance) {
1233         global $USER;
1235         return false;
1236     }
1238     /**
1239      * Enrol user into course via enrol instance.
1240      *
1241      * @param stdClass $instance
1242      * @param int $userid
1243      * @param int $roleid optional role id
1244      * @param int $timestart 0 means unknown
1245      * @param int $timeend 0 means forever
1246      * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
1247      * @return void
1248      */
1249     public function enrol_user(stdClass $instance, $userid, $roleid = NULL, $timestart = 0, $timeend = 0, $status = NULL) {
1250         global $DB, $USER, $CFG; // CFG necessary!!!
1252         if ($instance->courseid == SITEID) {
1253             throw new coding_exception('invalid attempt to enrol into frontpage course!');
1254         }
1256         $name = $this->get_name();
1257         $courseid = $instance->courseid;
1259         if ($instance->enrol !== $name) {
1260             throw new coding_exception('invalid enrol instance!');
1261         }
1262         $context = context_course::instance($instance->courseid, MUST_EXIST);
1264         $inserted = false;
1265         $updated  = false;
1266         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1267             //only update if timestart or timeend or status are different.
1268             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
1269                 $ue->timestart    = $timestart;
1270                 $ue->timeend      = $timeend;
1271                 if (!is_null($status)) {
1272                     $ue->status   = $status;
1273                 }
1274                 $ue->modifierid   = $USER->id;
1275                 $ue->timemodified = time();
1276                 $DB->update_record('user_enrolments', $ue);
1278                 $updated = true;
1279             }
1280         } else {
1281             $ue = new stdClass();
1282             $ue->enrolid      = $instance->id;
1283             $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
1284             $ue->userid       = $userid;
1285             $ue->timestart    = $timestart;
1286             $ue->timeend      = $timeend;
1287             $ue->modifierid   = $USER->id;
1288             $ue->timecreated  = time();
1289             $ue->timemodified = $ue->timecreated;
1290             $ue->id = $DB->insert_record('user_enrolments', $ue);
1292             $inserted = true;
1293         }
1295         if ($inserted) {
1296             // add extra info and trigger event
1297             $ue->courseid  = $courseid;
1298             $ue->enrol     = $name;
1299             events_trigger('user_enrolled', $ue);
1300         } else if ($updated) {
1301             $ue->courseid  = $courseid;
1302             $ue->enrol     = $name;
1303             events_trigger('user_enrol_modified', $ue);
1304             // resets current enrolment caches
1305             $context->mark_dirty();
1306         }
1308         if ($roleid) {
1309             // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
1310             if ($this->roles_protected()) {
1311                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1312             } else {
1313                 role_assign($roleid, $userid, $context->id);
1314             }
1315         }
1317         // reset current user enrolment caching
1318         if ($userid == $USER->id) {
1319             if (isset($USER->enrol['enrolled'][$courseid])) {
1320                 unset($USER->enrol['enrolled'][$courseid]);
1321             }
1322             if (isset($USER->enrol['tempguest'][$courseid])) {
1323                 unset($USER->enrol['tempguest'][$courseid]);
1324                 remove_temp_course_roles($context);
1325             }
1326         }
1327     }
1329     /**
1330      * Store user_enrolments changes and trigger event.
1331      *
1332      * @param stdClass $instance
1333      * @param int $userid
1334      * @param int $status
1335      * @param int $timestart
1336      * @param int $timeend
1337      * @return void
1338      */
1339     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1340         global $DB, $USER;
1342         $name = $this->get_name();
1344         if ($instance->enrol !== $name) {
1345             throw new coding_exception('invalid enrol instance!');
1346         }
1348         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1349             // weird, user not enrolled
1350             return;
1351         }
1353         $modified = false;
1354         if (isset($status) and $ue->status != $status) {
1355             $ue->status = $status;
1356             $modified = true;
1357         }
1358         if (isset($timestart) and $ue->timestart != $timestart) {
1359             $ue->timestart = $timestart;
1360             $modified = true;
1361         }
1362         if (isset($timeend) and $ue->timeend != $timeend) {
1363             $ue->timeend = $timeend;
1364             $modified = true;
1365         }
1367         if (!$modified) {
1368             // no change
1369             return;
1370         }
1372         $ue->modifierid = $USER->id;
1373         $DB->update_record('user_enrolments', $ue);
1374         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
1376         // trigger event
1377         $ue->courseid  = $instance->courseid;
1378         $ue->enrol     = $instance->name;
1379         events_trigger('user_enrol_modified', $ue);
1380     }
1382     /**
1383      * Unenrol user from course,
1384      * the last unenrolment removes all remaining roles.
1385      *
1386      * @param stdClass $instance
1387      * @param int $userid
1388      * @return void
1389      */
1390     public function unenrol_user(stdClass $instance, $userid) {
1391         global $CFG, $USER, $DB;
1392         require_once("$CFG->dirroot/group/lib.php");
1394         $name = $this->get_name();
1395         $courseid = $instance->courseid;
1397         if ($instance->enrol !== $name) {
1398             throw new coding_exception('invalid enrol instance!');
1399         }
1400         $context = context_course::instance($instance->courseid, MUST_EXIST);
1402         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1403             // weird, user not enrolled
1404             return;
1405         }
1407         // Remove all users groups linked to this enrolment instance.
1408         if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
1409             foreach ($gms as $gm) {
1410                 groups_remove_member($gm->groupid, $gm->userid);
1411             }
1412         }
1414         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1415         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1417         // add extra info and trigger event
1418         $ue->courseid  = $courseid;
1419         $ue->enrol     = $name;
1421         $sql = "SELECT 'x'
1422                   FROM {user_enrolments} ue
1423                   JOIN {enrol} e ON (e.id = ue.enrolid)
1424                  WHERE ue.userid = :userid AND e.courseid = :courseid";
1425         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1426             $ue->lastenrol = false;
1427             events_trigger('user_unenrolled', $ue);
1428             // user still has some enrolments, no big cleanup yet
1430         } else {
1431             // the big cleanup IS necessary!
1432             require_once("$CFG->libdir/gradelib.php");
1434             // remove all remaining roles
1435             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1437             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1438             groups_delete_group_members($courseid, $userid);
1440             grade_user_unenrol($courseid, $userid);
1442             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1444             $ue->lastenrol = true; // means user not enrolled any more
1445             events_trigger('user_unenrolled', $ue);
1446         }
1448         // reset all enrol caches
1449         $context->mark_dirty();
1451         // reset current user enrolment caching
1452         if ($userid == $USER->id) {
1453             if (isset($USER->enrol['enrolled'][$courseid])) {
1454                 unset($USER->enrol['enrolled'][$courseid]);
1455             }
1456             if (isset($USER->enrol['tempguest'][$courseid])) {
1457                 unset($USER->enrol['tempguest'][$courseid]);
1458                 remove_temp_course_roles($context);
1459             }
1460         }
1461     }
1463     /**
1464      * Forces synchronisation of user enrolments.
1465      *
1466      * This is important especially for external enrol plugins,
1467      * this function is called for all enabled enrol plugins
1468      * right after every user login.
1469      *
1470      * @param object $user user record
1471      * @return void
1472      */
1473     public function sync_user_enrolments($user) {
1474         // override if necessary
1475     }
1477     /**
1478      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1479      * @param int $courseid
1480      * @return moodle_url page url
1481      */
1482     public function get_newinstance_link($courseid) {
1483         // override for most plugins, check if instance already exists in cases only one instance is supported
1484         return NULL;
1485     }
1487     /**
1488      * Is it possible to delete enrol instance via standard UI?
1489      *
1490      * @param object $instance
1491      * @return bool
1492      */
1493     public function instance_deleteable($instance) {
1494         return true;
1495     }
1497     /**
1498      * Returns link to manual enrol UI if exists.
1499      * Does the access control tests automatically.
1500      *
1501      * @param object $instance
1502      * @return moodle_url
1503      */
1504     public function get_manual_enrol_link($instance) {
1505         return NULL;
1506     }
1508     /**
1509      * Returns list of unenrol links for all enrol instances in course.
1510      *
1511      * @param int $instance
1512      * @return moodle_url or NULL if self unenrolment not supported
1513      */
1514     public function get_unenrolself_link($instance) {
1515         global $USER, $CFG, $DB;
1517         $name = $this->get_name();
1518         if ($instance->enrol !== $name) {
1519             throw new coding_exception('invalid enrol instance!');
1520         }
1522         if ($instance->courseid == SITEID) {
1523             return NULL;
1524         }
1526         if (!enrol_is_enabled($name)) {
1527             return NULL;
1528         }
1530         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1531             return NULL;
1532         }
1534         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1535             return NULL;
1536         }
1538         $context = context_course::instance($instance->courseid, MUST_EXIST);
1540         if (!has_capability("enrol/$name:unenrolself", $context)) {
1541             return NULL;
1542         }
1544         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1545             return NULL;
1546         }
1548         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1549     }
1551     /**
1552      * Adds enrol instance UI to course edit form
1553      *
1554      * @param object $instance enrol instance or null if does not exist yet
1555      * @param MoodleQuickForm $mform
1556      * @param object $data
1557      * @param object $context context of existing course or parent category if course does not exist
1558      * @return void
1559      */
1560     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1561         // override - usually at least enable/disable switch, has to add own form header
1562     }
1564     /**
1565      * Validates course edit form data
1566      *
1567      * @param object $instance enrol instance or null if does not exist yet
1568      * @param array $data
1569      * @param object $context context of existing course or parent category if course does not exist
1570      * @return array errors array
1571      */
1572     public function course_edit_validation($instance, array $data, $context) {
1573         return array();
1574     }
1576     /**
1577      * Called after updating/inserting course.
1578      *
1579      * @param bool $inserted true if course just inserted
1580      * @param object $course
1581      * @param object $data form data
1582      * @return void
1583      */
1584     public function course_updated($inserted, $course, $data) {
1585         if ($inserted) {
1586             if ($this->get_config('defaultenrol')) {
1587                 $this->add_default_instance($course);
1588             }
1589         }
1590     }
1592     /**
1593      * Add new instance of enrol plugin.
1594      * @param object $course
1595      * @param array instance fields
1596      * @return int id of new instance, null if can not be created
1597      */
1598     public function add_instance($course, array $fields = NULL) {
1599         global $DB;
1601         if ($course->id == SITEID) {
1602             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1603         }
1605         $instance = new stdClass();
1606         $instance->enrol          = $this->get_name();
1607         $instance->status         = ENROL_INSTANCE_ENABLED;
1608         $instance->courseid       = $course->id;
1609         $instance->enrolstartdate = 0;
1610         $instance->enrolenddate   = 0;
1611         $instance->timemodified   = time();
1612         $instance->timecreated    = $instance->timemodified;
1613         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1615         $fields = (array)$fields;
1616         unset($fields['enrol']);
1617         unset($fields['courseid']);
1618         unset($fields['sortorder']);
1619         foreach($fields as $field=>$value) {
1620             $instance->$field = $value;
1621         }
1623         return $DB->insert_record('enrol', $instance);
1624     }
1626     /**
1627      * Add new instance of enrol plugin with default settings,
1628      * called when adding new instance manually or when adding new course.
1629      *
1630      * Not all plugins support this.
1631      *
1632      * @param object $course
1633      * @return int id of new instance or null if no default supported
1634      */
1635     public function add_default_instance($course) {
1636         return null;
1637     }
1639     /**
1640      * Update instance status
1641      *
1642      * Override when plugin needs to do some action when enabled or disabled.
1643      *
1644      * @param stdClass $instance
1645      * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
1646      * @return void
1647      */
1648     public function update_status($instance, $newstatus) {
1649         global $DB;
1651         $instance->status = $newstatus;
1652         $DB->update_record('enrol', $instance);
1654         // invalidate all enrol caches
1655         $context = context_course::instance($instance->courseid);
1656         $context->mark_dirty();
1657     }
1659     /**
1660      * Delete course enrol plugin instance, unenrol all users.
1661      * @param object $instance
1662      * @return void
1663      */
1664     public function delete_instance($instance) {
1665         global $DB;
1667         $name = $this->get_name();
1668         if ($instance->enrol !== $name) {
1669             throw new coding_exception('invalid enrol instance!');
1670         }
1672         //first unenrol all users
1673         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1674         foreach ($participants as $participant) {
1675             $this->unenrol_user($instance, $participant->userid);
1676         }
1677         $participants->close();
1679         // now clean up all remainders that were not removed correctly
1680         $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1681         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1682         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1684         // finally drop the enrol row
1685         $DB->delete_records('enrol', array('id'=>$instance->id));
1687         // invalidate all enrol caches
1688         $context = context_course::instance($instance->courseid);
1689         $context->mark_dirty();
1690     }
1692     /**
1693      * Creates course enrol form, checks if form submitted
1694      * and enrols user if necessary. It can also redirect.
1695      *
1696      * @param stdClass $instance
1697      * @return string html text, usually a form in a text box
1698      */
1699     public function enrol_page_hook(stdClass $instance) {
1700         return null;
1701     }
1703     /**
1704      * Adds navigation links into course admin block.
1705      *
1706      * By defaults looks for manage links only.
1707      *
1708      * @param navigation_node $instancesnode
1709      * @param stdClass $instance
1710      * @return void
1711      */
1712     public function add_course_navigation($instancesnode, stdClass $instance) {
1713         // usually adds manage users
1714     }
1716     /**
1717      * Returns edit icons for the page with list of instances
1718      * @param stdClass $instance
1719      * @return array
1720      */
1721     public function get_action_icons(stdClass $instance) {
1722         return array();
1723     }
1725     /**
1726      * Reads version.php and determines if it is necessary
1727      * to execute the cron job now.
1728      * @return bool
1729      */
1730     public function is_cron_required() {
1731         global $CFG;
1733         $name = $this->get_name();
1734         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1735         $plugin = new stdClass();
1736         include($versionfile);
1737         if (empty($plugin->cron)) {
1738             return false;
1739         }
1740         $lastexecuted = $this->get_config('lastcron', 0);
1741         if ($lastexecuted + $plugin->cron < time()) {
1742             return true;
1743         } else {
1744             return false;
1745         }
1746     }
1748     /**
1749      * Called for all enabled enrol plugins that returned true from is_cron_required().
1750      * @return void
1751      */
1752     public function cron() {
1753     }
1755     /**
1756      * Called when user is about to be deleted
1757      * @param object $user
1758      * @return void
1759      */
1760     public function user_delete($user) {
1761         global $DB;
1763         $sql = "SELECT e.*
1764                   FROM {enrol} e
1765                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
1766                  WHERE e.enrol = :name AND ue.userid = :userid";
1767         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1769         $rs = $DB->get_recordset_sql($sql, $params);
1770         foreach($rs as $instance) {
1771             $this->unenrol_user($instance, $user->id);
1772         }
1773         $rs->close();
1774     }
1776     /**
1777      * Returns an enrol_user_button that takes the user to a page where they are able to
1778      * enrol users into the managers course through this plugin.
1779      *
1780      * Optional: If the plugin supports manual enrolments it can choose to override this
1781      * otherwise it shouldn't
1782      *
1783      * @param course_enrolment_manager $manager
1784      * @return enrol_user_button|false
1785      */
1786     public function get_manual_enrol_button(course_enrolment_manager $manager) {
1787         return false;
1788     }
1790     /**
1791      * Gets an array of the user enrolment actions
1792      *
1793      * @param course_enrolment_manager $manager
1794      * @param stdClass $ue
1795      * @return array An array of user_enrolment_actions
1796      */
1797     public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
1798         return array();
1799     }
1801     /**
1802      * Returns true if the plugin has one or more bulk operations that can be performed on
1803      * user enrolments.
1804      *
1805      * @param course_enrolment_manager $manager
1806      * @return bool
1807      */
1808     public function has_bulk_operations(course_enrolment_manager $manager) {
1809        return false;
1810     }
1812     /**
1813      * Return an array of enrol_bulk_enrolment_operation objects that define
1814      * the bulk actions that can be performed on user enrolments by the plugin.
1815      *
1816      * @param course_enrolment_manager $manager
1817      * @return array
1818      */
1819     public function get_bulk_operations(course_enrolment_manager $manager) {
1820         return array();
1821     }
1823     /**
1824      * Send expiry notifications.
1825      *
1826      * Plugin that wants to have expiry notification MUST implement following:
1827      * - expirynotifyhour plugin setting,
1828      * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
1829      * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
1830      *   expirymessageenrolledsubject and expirymessageenrolledbody),
1831      * - expiry_notification provider in db/messages.php,
1832      * - upgrade code that sets default thresholds for existing courses (should be 1 day),
1833      * - something that calls this method, such as cron.
1834      *
1835      * @param bool $verbose verbose CLI output
1836      */
1837     public function send_expiry_notifications($verbose = false) {
1838         global $DB, $CFG;
1840         // Unfortunately this may take a long time, it should not be interrupted,
1841         // otherwise users get duplicate notification.
1843         @set_time_limit(0);
1844         raise_memory_limit(MEMORY_HUGE);
1846         $name = $this->get_name();
1848         $expirynotifylast = $this->get_config('expirynotifylast', 0);
1849         $expirynotifyhour = $this->get_config('expirynotifyhour');
1850         if (is_null($expirynotifyhour)) {
1851             debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
1852             return;
1853         }
1855         $timenow = time();
1856         $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
1858         if ($expirynotifylast > $notifytime) {
1859             if ($verbose) {
1860                 mtrace($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
1861             }
1862             return;
1863         } else if ($timenow < $notifytime) {
1864             if ($verbose) {
1865                 mtrace($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
1866             }
1867             return;
1868         }
1870         if ($verbose) {
1871             mtrace('Processing '.$name.' enrolment expiration notifications...');
1872         }
1874         // Notify users responsible for enrolment once every day.
1875         $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
1876                   FROM {user_enrolments} ue
1877                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
1878                   JOIN {course} c ON (c.id = e.courseid)
1879                   JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
1880                  WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
1881               ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
1882         $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
1884         $rs = $DB->get_recordset_sql($sql, $params);
1886         $lastenrollid = 0;
1887         $users = array();
1889         foreach($rs as $ue) {
1890             if ($lastenrollid and $lastenrollid != $ue->enrolid) {
1891                 $this->notify_expiry_enroller($lastenrollid, $users, $verbose);
1892                 $users = array();
1893             }
1894             $lastenrollid = $ue->enrolid;
1896             $enroller = $this->get_enroller($ue->enrolid);
1897             $context = context_course::instance($ue->courseid);
1899             $user = $DB->get_record('user', array('id'=>$ue->userid));
1901             $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
1903             if (!$ue->notifyall) {
1904                 continue;
1905             }
1907             if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
1908                 // Notify enrolled users only once at the start of the threshold.
1909                 if ($verbose) {
1910                     mtrace("  user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone));
1911                 }
1912                 continue;
1913             }
1915             $this->notify_expiry_enrolled($user, $ue, $verbose);
1916         }
1917         $rs->close();
1919         if ($lastenrollid and $users) {
1920             $this->notify_expiry_enroller($lastenrollid, $users, $verbose);
1921         }
1923         if ($verbose) {
1924             mtrace('...notification processing finished.');
1925         }
1926         $this->set_config('expirynotifylast', $timenow);
1927     }
1929     /**
1930      * Returns the user who is responsible for enrolments for given instance.
1931      *
1932      * Override if plugin knows anybody better than admin.
1933      *
1934      * @param int $instanceid enrolment instance id
1935      * @return stdClass user record
1936      */
1937     protected function get_enroller($instanceid) {
1938         return get_admin();
1939     }
1941     /**
1942      * Notify user about incoming expiration of their enrolment,
1943      * it is called only if notification of enrolled users (aka students) is enabled in course.
1944      *
1945      * This is executed only once for each expiring enrolment right
1946      * at the start of the expiration threshold.
1947      *
1948      * @param stdClass $user
1949      * @param stdClass $ue
1950      * @param bool $verbose
1951      */
1952     protected function notify_expiry_enrolled($user, $ue, $verbose) {
1953         global $CFG, $SESSION;
1955         $name = $this->get_name();
1957         // Some nasty hackery to get strings and dates localised for target user.
1958         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
1959         if (get_string_manager()->translation_exists($user->lang, false)) {
1960             $SESSION->lang = $user->lang;
1961             moodle_setlocale();
1962         }
1964         $enroller = $this->get_enroller($ue->enrolid);
1965         $context = context_course::instance($ue->courseid);
1967         $a = new stdClass();
1968         $a->course   = format_string($ue->fullname, true, array('context'=>$context));
1969         $a->user     = fullname($user, true);
1970         $a->timeend  = userdate($ue->timeend, '', $user->timezone);
1971         $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
1973         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
1974         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
1976         $message = new stdClass();
1977         $message->notification      = 1;
1978         $message->component         = 'enrol_'.$name;
1979         $message->name              = 'expiry_notification';
1980         $message->userfrom          = $enroller;
1981         $message->userto            = $user;
1982         $message->subject           = $subject;
1983         $message->fullmessage       = $body;
1984         $message->fullmessageformat = FORMAT_MARKDOWN;
1985         $message->fullmessagehtml   = markdown_to_html($body);
1986         $message->smallmessage      = $subject;
1987         $message->contexturlname    = $a->course;
1988         $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
1990         if (message_send($message)) {
1991             if ($verbose) {
1992                 mtrace("  notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone));
1993             }
1994         } else {
1995             if ($verbose) {
1996                 mtrace("  error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone));
1997             }
1998         }
2000         if ($SESSION->lang !== $sessionlang) {
2001             $SESSION->lang = $sessionlang;
2002             moodle_setlocale();
2003         }
2004     }
2006     /**
2007      * Notify person responsible for enrolments that some user enrolments will be expired soon,
2008      * it is called only if notification of enrollers (aka teachers) is enabled in course.
2009      *
2010      * This is called repeatedly every day for each course if there are any pending expiration
2011      * in the expiration threshold.
2012      *
2013      * @param int $eid
2014      * @param array $users
2015      * @param bool $verbose
2016      */
2017     protected function notify_expiry_enroller($eid, $users, $verbose) {
2018         global $DB, $SESSION;
2020         $name = $this->get_name();
2022         $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
2023         $context = context_course::instance($instance->courseid);
2024         $course = $DB->get_record('course', array('id'=>$instance->courseid));
2026         $enroller = $this->get_enroller($instance->id);
2027         $admin = get_admin();
2029         // Some nasty hackery to get strings and dates localised for target user.
2030         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
2031         if (get_string_manager()->translation_exists($enroller->lang, false)) {
2032             $SESSION->lang = $enroller->lang;
2033             moodle_setlocale();
2034         }
2036         foreach($users as $key=>$info) {
2037             $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
2038         }
2040         $a = new stdClass();
2041         $a->course    = format_string($course->fullname, true, array('context'=>$context));
2042         $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
2043         $a->users     = implode("\n", $users);
2044         $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid));
2046         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
2047         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
2049         $message = new stdClass();
2050         $message->notification      = 1;
2051         $message->component         = 'enrol_'.$name;
2052         $message->name              = 'expiry_notification';
2053         $message->userfrom          = $admin;
2054         $message->userto            = $enroller;
2055         $message->subject           = $subject;
2056         $message->fullmessage       = $body;
2057         $message->fullmessageformat = FORMAT_MARKDOWN;
2058         $message->fullmessagehtml   = markdown_to_html($body);
2059         $message->smallmessage      = $subject;
2060         $message->contexturlname    = $a->course;
2061         $message->contexturl        = $a->extendurl;
2063         if (message_send($message)) {
2064             if ($verbose) {
2065                 mtrace("  notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid");
2066             }
2067         } else {
2068             if ($verbose) {
2069                 mtrace("  error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid");
2070             }
2071         }
2073         if ($SESSION->lang !== $sessionlang) {
2074             $SESSION->lang = $sessionlang;
2075             moodle_setlocale();
2076         }
2077     }
2079     /**
2080      * Automatic enrol sync executed during restore.
2081      * Useful for automatic sync by course->idnumber or course category.
2082      * @param stdClass $course course record
2083      */
2084     public function restore_sync_course($course) {
2085         // Override if necessary.
2086     }
2088     /**
2089      * Restore instance and map settings.
2090      *
2091      * @param restore_enrolments_structure_step $step
2092      * @param stdClass $data
2093      * @param stdClass $course
2094      * @param int $oldid
2095      */
2096     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
2097         // Do not call this from overridden methods, restore and set new id there.
2098         $step->set_mapping('enrol', $oldid, 0);
2099     }
2101     /**
2102      * Restore user enrolment.
2103      *
2104      * @param restore_enrolments_structure_step $step
2105      * @param stdClass $data
2106      * @param stdClass $instance
2107      * @param int $oldinstancestatus
2108      * @param int $userid
2109      */
2110     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
2111         // Override as necessary if plugin supports restore of enrolments.
2112     }
2114     /**
2115      * Restore role assignment.
2116      *
2117      * @param stdClass $instance
2118      * @param int $roleid
2119      * @param int $userid
2120      * @param int $contextid
2121      */
2122     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
2123         // No role assignment by default, override if necessary.
2124     }
2126     /**
2127      * Restore user group membership.
2128      * @param stdClass $instance
2129      * @param int $groupid
2130      * @param int $userid
2131      */
2132     public function restore_group_member($instance, $groupid, $userid) {
2133         // Implement if you want to restore protected group memberships,
2134         // usually this is not necessary because plugins should be able to recreate the memberships automatically.
2135     }