MDL-22884 fixed sloppy userid related bugs in enrol code - big thanks to Andrew for...
[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   moodlecore
23  * @copyright 2010 Petr Skoda {@link http://skodak.org}
24  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
28 /** Course enrol instance enabled. (used in enrol->status) */
29 define('ENROL_INSTANCE_ENABLED', 0);
31 /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
32 define('ENROL_INSTANCE_DISABLED', 1);
34 /** User is active participant (used in user_enrolments->status)*/
35 define('ENROL_USER_ACTIVE', 0);
37 /** User participation in course is suspended (used in user_enrolments->status) */
38 define('ENROL_USER_SUSPENDED', 1);
40 /** Enrol info is cached for this number of seconds in require_login() */
41 define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
43 /**
44  * Returns instances of enrol plugins
45  * @param bool $enable return enabled only
46  * @return array of enrol plugins name=>instance
47  */
48 function enrol_get_plugins($enabled) {
49     global $CFG;
51     $result = array();
53     if ($enabled) {
54         // sorted by enabled plugin order
55         $enabled = explode(',', $CFG->enrol_plugins_enabled);
56         $plugins = array();
57         foreach ($enabled as $plugin) {
58             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
59         }
60     } else {
61         // sorted alphabetically
62         $plugins = get_plugin_list('enrol');
63         ksort($plugins);
64     }
66     foreach ($plugins as $plugin=>$location) {
67         if (!file_exists("$location/lib.php")) {
68             continue;
69         }
70         include_once("$location/lib.php");
71         $class = "enrol_{$plugin}_plugin";
72         if (!class_exists($class)) {
73             continue;
74         }
76         $result[$plugin] = new $class();
77     }
79     return $result;
80 }
82 /**
83  * Returns instance of enrol plugin
84  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
85  * @return enrol_plugin
86  */
87 function enrol_get_plugin($name) {
88     global $CFG;
90     if ($name !== clean_param($name, PARAM_SAFEDIR)) {
91         // ignore malformed plugin names completely
92         return null;
93     }
95     $location = "$CFG->dirroot/enrol/$name";
97     if (!file_exists("$location/lib.php")) {
98         return null;
99     }
100     include_once("$location/lib.php");
101     $class = "enrol_{$name}_plugin";
102     if (!class_exists($class)) {
103         return null;
104     }
106     return new $class();
109 /**
110  * Returns enrolment instances in given course.
111  * @param int $courseid
112  * @param bool $enabled
113  * @return array of enrol instances
114  */
115 function enrol_get_instances($courseid, $enabled) {
116     global $DB, $CFG;
118     if (!$enabled) {
119         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
120     }
122     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
124     $enabled = explode(',', $CFG->enrol_plugins_enabled);
125     foreach ($result as $key=>$instance) {
126         if (!in_array($instance->enrol, $enabled)) {
127             unset($result[$key]);
128             continue;
129         }
130         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
131             // broken plugin
132             unset($result[$key]);
133             continue;
134         }
135     }
137     return $result;
140 /**
141  * Checks if a given plugin is in the list of enabled enrolment plugins.
142  *
143  * @param string $enrol Enrolment plugin name
144  * @return boolean Whether the plugin is enabled
145  */
146 function enrol_is_enabled($enrol) {
147     global $CFG;
149     if (empty($CFG->enrol_plugins_enabled)) {
150         return false;
151     }
152     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
155 /**
156  * Check all the login enrolment information for the given user object
157  * by querying the enrolment plugins
158  *
159  * @param object $user
160  * @return void
161  */
162 function enrol_check_plugins($user) {
163     global $CFG;
165     if (empty($user->id) or isguestuser($user)) {
166         // shortcut - there is no enrolment work for guests and not-logged-in users
167         return;
168     }
170     static $inprogress = array();  // To prevent this function being called more than once in an invocation
172     if (!empty($inprogress[$user->id])) {
173         return;
174     }
176     $inprogress[$user->id] = true;  // Set the flag
178     $enabled = enrol_get_plugins(true);
180     foreach($enabled as $enrol) {
181         $enrol->sync_user_enrolments($user);
182     }
184     unset($inprogress[$user->id]);  // Unset the flag
187 /**
188  * This function adds necessary enrol plugins UI into the course edit form.
189  *
190  * @param MoodleQuickForm $mform
191  * @param object $data course edit form data
192  * @param object $context context of existing course or parent category if course does not exist
193  * @return void
194  */
195 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
196     $plugins = enrol_get_plugins(true);
197     if (!empty($data->id)) {
198         $instances = enrol_get_instances($data->id, false);
199         foreach ($instances as $instance) {
200             if (!isset($plugins[$instance->enrol])) {
201                 continue;
202             }
203             $plugin = $plugins[$instance->enrol];
204             $plugin->course_edit_form($instance, $mform, $data, $context);
205         }
206     } else {
207         foreach ($plugins as $plugin) {
208             $plugin->course_edit_form(NULL, $mform, $data, $context);
209         }
210     }
213 /**
214  * Validate course edit form data
215  *
216  * @param array $data raw form data
217  * @param object $context context of existing course or parent category if course does not exist
218  * @return array errors array
219  */
220 function enrol_course_edit_validation(array $data, $context) {
221     $errors = array();
222     $plugins = enrol_get_plugins(true);
224     if (!empty($data['id'])) {
225         $instances = enrol_get_instances($data['id'], false);
226         foreach ($instances as $instance) {
227             if (!isset($plugins[$instance->enrol])) {
228                 continue;
229             }
230             $plugin = $plugins[$instance->enrol];
231             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
232         }
233     } else {
234         foreach ($plugins as $plugin) {
235             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
236         }
237     }
239     return $errors;
242 /**
243  * Update enrol instances after course edit form submission
244  * @param bool $inserted true means new course added, false course already existed
245  * @param object $course
246  * @param object $data form data
247  * @return void
248  */
249 function enrol_course_updated($inserted, $course, $data) {
250     global $DB, $CFG;
252     $plugins = enrol_get_plugins(true);
254     foreach ($plugins as $plugin) {
255         $plugin->course_updated($inserted, $course, $data);
256     }
259 /**
260  * Add navigation nodes
261  * @param navigation_node $coursenode
262  * @param object $course
263  * @return void
264  */
265 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
267     $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
269     $instances = enrol_get_instances($course->id, true);
270     $plugins   = enrol_get_plugins(true);
272     // we do not want to break all course pages if there is some borked enrol plugin, right?
273     foreach ($instances as $k=>$instance) {
274         if (!isset($plugins[$instance->enrol])) {
275             unset($instances[$k]);
276         }
277     }
279     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER);
281     if ($course->id != SITEID) {
282         // list all participants - allows assing roles, groups, etc.
283         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
284             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
285             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/users', ''));
286         }
288         // manage enrol plugin instances
289         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
290             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
291         } else {
292             $url = NULL;
293         }
294         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url);
296         // each instance decides how to configure itself or how many other nav items are exposed
297         foreach ($instances as $instance) {
298             if (!isset($plugins[$instance->enrol])) {
299                 continue;
300             }
301             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
302         }
304         if (!$url) {
305             $instancesnode->trim_if_empty();
306         }
307     }
309     // Manage groups in this course or even frontpage
310     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
311         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
312         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
313     }
315      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
316         // Override roles
317         if (has_capability('moodle/role:review', $coursecontext)) {
318             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
319         } else {
320             $url = NULL;
321         }
322         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url);
324         // Add assign or override roles if allowed
325         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
326             if (has_capability('moodle/role:assign', $coursecontext)) {
327                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
328                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/roles', ''));
329             }
330         }
331         // Check role permissions
332         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
333             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
334             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/checkpermissions', ''));
335         }
336      }
338      // Deal somehow with users that are not enrolled but still got a role somehow
339     if ($course->id != SITEID) {
340         //TODO, create some new UI for role assignments at course level
341         if (has_capability('moodle/role:assign', $coursecontext)) {
342             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
343             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/roles', ''));
344         }
345     }
347     // just in case nothing was actually added
348     $usersnode->trim_if_empty();
350     if ($course->id != SITEID) {
351         // Unenrol link
352         $unenrolprinted = false;
353         foreach ($instances as $instance) {
354             if (!isset($plugins[$instance->enrol])) {
355                 continue;
356             }
357             $plugin = $plugins[$instance->enrol];
358             if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
359                 $coursenode->add(get_string('unenrolme', 'core_enrol', format_string($course->shortname)), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
360                 $unenrolprinted = true;
361                 //TODO. deal with multiple unenrol links - not likely case, but still...
362             }
363         }
364         // Enrol link
365         if (!$unenrolprinted and !is_viewing($coursecontext) and !is_enrolled($coursecontext)) {
366             $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
367             $coursenode->add(get_string('enrolme', 'core_enrol', format_string($course->shortname)), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
368         }
369     }
372 /**
373  * Returns list of courses current $USER is enrolled in and can access
374  *
375  * - $fields is an array of field names to ADD
376  *   so name the fields you really need, which will
377  *   be added and uniq'd
378  *
379  * @param strin|array $fields
380  * @param string $sort
381  * @param int $limit max number of courses
382  * @return array
383  */
384 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
385     global $DB, $USER;
387     // Guest account does not have any courses
388     if (isguestuser() or !isloggedin()) {
389         return(array());
390     }
392     $basefields = array('id', 'category', 'sortorder',
393                         'shortname', 'fullname', 'idnumber',
394                         'startdate', 'visible',
395                         'groupmode', 'groupmodeforce');
397     if (empty($fields)) {
398         $fields = $basefields;
399     } else if (is_string($fields)) {
400         // turn the fields from a string to an array
401         $fields = explode(',', $fields);
402         $fields = array_map('trim', $fields);
403         $fields = array_unique(array_merge($basefields, $fields));
404     } else if (is_array($fields)) {
405         $fields = array_unique(array_merge($basefields, $fields));
406     } else {
407         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
408     }
409     if (in_array('*', $fields)) {
410         $fields = array('*');
411     }
413     $orderby = "";
414     $sort    = trim($sort);
415     if (!empty($sort)) {
416         $rawsorts = explode(',', $sort);
417         $sorts = array();
418         foreach ($rawsorts as $rawsort) {
419             $rawsort = trim($rawsort);
420             if (strpos($rawsort, 'c.') === 0) {
421                 $rawsort = substr($rawsort, 2);
422             }
423             $sorts[] = trim($rawsort);
424         }
425         $sort = 'c.'.implode(',c.', $sorts);
426         $orderby = "ORDER BY $sort";
427     }
429     $wheres = array("c.id <> :siteid");
430     $params = array('siteid'=>SITEID);
432     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
433         // list _only_ this course - anything else is asking for trouble...
434         $wheres[] = "courseid = :loginas";
435         $params['loginas'] = $USER->loginascontext->instanceid;
436     }
438     $coursefields = 'c.' .join(',c.', $fields);
439     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
440     $wheres = " AND ".implode(" AND ", $wheres);
442     $sql = "SELECT DISTINCT $coursefields $ccselect
443               FROM {course} c
444               JOIN {enrol} e ON (e.courseid = c.id AND e.status = :enabled)
445               JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid AND ue.status = :active)
446            $ccjoin
447              WHERE ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
448                    $wheres
449           $orderby";
450     $params['userid']  = $USER->id;
451     $params['active']  = ENROL_USER_ACTIVE;
452     $params['enabled'] = ENROL_INSTANCE_ENABLED;
453     $params['now1']    = round(time(), -2); // improves db caching
454     $params['now2']    = $params['now1'];
456     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
458     // preload contexts and check visibility
459     foreach ($courses as $id=>$course) {
460         context_instance_preload($course);
461         if (!$course->visible) {
462             if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
463                 unset($course[$id]);
464                 continue;
465             }
466             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
467                 unset($course[$id]);
468                 continue;
469             }
470         }
471         $courses[$id] = $course;
472     }
474     //wow! Is that really all? :-D
476     return $courses;
479 /**
480  * Returns list of courses user is enrolled into.
481  *
482  * - $fields is an array of fieldnames to ADD
483  *   so name the fields you really need, which will
484  *   be added and uniq'd
485  *
486  * @param int $userid
487  * @param bool $onlyactive return only active enrolments in courses user may see
488  * @param strin|array $fields
489  * @param string $sort
490  * @return array
491  */
492 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
493     global $DB;
495     // Guest account does not have any courses
496     if (isguestuser($userid) or empty($userid)) {
497         return(array());
498     }
500     $basefields = array('id', 'category', 'sortorder',
501                         'shortname', 'fullname', 'idnumber',
502                         'startdate', 'visible',
503                         'groupmode', 'groupmodeforce');
505     if (empty($fields)) {
506         $fields = $basefields;
507     } else if (is_string($fields)) {
508         // turn the fields from a string to an array
509         $fields = explode(',', $fields);
510         $fields = array_map('trim', $fields);
511         $fields = array_unique(array_merge($basefields, $fields));
512     } else if (is_array($fields)) {
513         $fields = array_unique(array_merge($basefields, $fields));
514     } else {
515         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
516     }
517     if (in_array('*', $fields)) {
518         $fields = array('*');
519     }
521     $orderby = "";
522     $sort    = trim($sort);
523     if (!empty($sort)) {
524         $rawsorts = explode(',', $sort);
525         $sorts = array();
526         foreach ($rawsorts as $rawsort) {
527             $rawsort = trim($rawsort);
528             if (strpos($rawsort, 'c.') === 0) {
529                 $rawsort = substr($rawsort, 2);
530             }
531             $sorts[] = trim($rawsort);
532         }
533         $sort = 'c.'.implode(',c.', $sorts);
534         $orderby = "ORDER BY $sort";
535     }
537     $wheres = array("c.id <> :siteid");
538     $params = array('siteid'=>SITEID);
540     if ($onlyactive) {
541         $wheres[] = "ue.status = :active";
542         $wheres[] = "e.status = :enabled";
543         $wheres[] = "ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
544         $params['now1']    = round(time(), -2); // improves db caching
545         $params['now2']    = $params['now1'];
546         $params['active']  = ENROL_USER_ACTIVE;
547         $params['enabled'] = ENROL_INSTANCE_ENABLED;
548     }
550     $coursefields = 'c.' .join(',c.', $fields);
551     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
552     $wheres = "WHERE ".implode(" AND ", $wheres);
554     $sql = "SELECT DISTINCT $coursefields $ccselect
555               FROM {course} c
556               JOIN {enrol} e ON (e.courseid = c.id)
557               JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
558            $ccjoin
559            $wheres
560           $orderby";
561     $params['userid']  = $userid;
563     $courses = $DB->get_records_sql($sql, $params);
565     // preload contexts and check visibility
566     foreach ($courses as $id=>$course) {
567         context_instance_preload($course);
568         if ($onlyactive) {
569             if (!$course->visible) {
570                 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
571                     unset($course[$id]);
572                     continue;
573                 }
574                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
575                     unset($course[$id]);
576                     continue;
577                 }
578             }
579         }
580         $courses[$id] = $course;
581     }
583     //wow! Is that really all? :-D
585     return $courses;
589 /**
590  * Called when user is about to be deleted.
591  * @param object $user
592  * @return void
593  */
594 function enrol_user_delete($user) {
595     global $DB;
597     $plugins = enrol_get_plugins(true);
598     foreach ($plugins as $plugin) {
599         $plugin->user_delete($user);
600     }
602     // force cleanup of all broken enrolments
603     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
606 /**
607  * Try to enrol user via default internal auth plugin.
608  *
609  * For now this is always using the manual enrol plugin...
610  *
611  * @param $courseid
612  * @param $userid
613  * @param $roleid
614  * @param $timestart
615  * @param $timeend
616  * @return bool success
617  */
618 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
619     global $DB;
621     //note: this is hardcoded to manual plugin for now
623     if (!enrol_is_enabled('manual')) {
624         return false;
625     }
627     if (!$enrol = enrol_get_plugin('manual')) {
628         return false;
629     }
630     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
631         return false;
632     }
633     $instance = reset($instances);
635     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
637     return true;
640 /**
641  * All enrol plugins should be based on this class,
642  * this is also the main source of documentation.
643  */
644 abstract class enrol_plugin {
645     protected $config = null;
647     /**
648      * Returns name of this enrol plugin
649      * @return string
650      */
651     public function get_name() {
652         // second word in class is always enrol name
653         $words = explode('_', get_class($this));
654         return $words[1];
655     }
657     /**
658      * Returns localised name of enrol instance
659      *
660      * @param object $instance (null is accepted too)
661      * @return string
662      */
663     public function get_instance_name($instance) {
664         if (empty($instance->name)) {
665             $enrol = $this->get_name();
666             return get_string('pluginname', 'enrol_'.$enrol);
667         } else {
668             return format_string($instance->name);
669         }
670     }
672     /**
673      * Makes sure config is loaded and cached.
674      * @return void
675      */
676     protected function load_config() {
677         if (!isset($this->config)) {
678             $name = $this->get_name();
679             if (!$config = get_config("enrol_$name")) {
680                 $config = new object();
681             }
682             $this->config = $config;
683         }
684     }
686     /**
687      * Returns plugin config value
688      * @param  string $name
689      * @param  string $default value if config does not exist yet
690      * @return string value or default
691      */
692     public function get_config($name, $default = NULL) {
693         $this->load_config();
694         return isset($this->config->$name) ? $this->config->$name : $default;
695     }
697     /**
698      * Sets plugin config value
699      * @param  string $name name of config
700      * @param  string $value string config value, null means delete
701      * @return string value
702      */
703     public function set_config($name, $value) {
704         $name = $this->get_name();
705         $this->load_config();
706         if ($value === NULL) {
707             unset($this->config->$name);
708         } else {
709             $this->config->$name = $value;
710         }
711         set_config($name, $value, "enrol_$name");
712     }
714     /**
715      * Does this plugin assign protected roles are can they be manually removed?
716      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
717      */
718     public function roles_protected() {
719         return true;
720     }
722     /**
723      * Does this plugin allow manual unenrolments?
724      *
725      * @param stdClass $instance course enrol instance
726      * ALl plugins allowing this must implement 'enrol/xxx:unenrol' capability
727      *
728      * @return bool - true means anybody may unenrol others freely, trues means nobody may touch user_enrolments
729      */
730     public function allow_unenrol(stdClass $instance) {
731         return false;
732     }
734     /**
735      * Does this plugin allow manual changes in user_enrolments table?
736      *
737      * ALl plugins allowing this must implement 'enrol/xxx:manage' capability
738      *
739      * @param stdClass $instance course enrol instance
740      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
741      */
742     public function allow_manage(stdClass $instance) {
743         return false;
744     }
746     /**
747      * Attempt to automatically enrol current user in course without any interaction,
748      * calling code has to make sure the plugin and instance are active.
749      *
750      * @param stdClass $instance course enrol instance
751      * @param stdClass $user record
752      * @return bool|int false means not enrolled, integer means timeend
753      */
754     public function try_autoenrol(stdClass $instance) {
755         global $USER;
757         return false;
758     }
760     /**
761      * Attempt to automatically gain temporary guest access to course,
762      * calling code has to make sure the plugin and instance are active.
763      *
764      * @param stdClass $instance course enrol instance
765      * @param stdClass $user record
766      * @return bool|int false means no guest access, integer means timeend
767      */
768     public function try_guestaccess(stdClass $instance) {
769         global $USER;
771         return false;
772     }
774     /**
775      * Enrol user into course via enrol instance.
776      *
777      * @param stdClass $instance
778      * @param int $userid
779      * @param int $roleid optional role id
780      * @param int $timestart
781      * @param int $timeend
782      * @return void
783      */
784     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
785         global $DB, $USER, $CFG; // CFG necessary!!!
787         if ($instance->courseid == SITEID) {
788             throw new coding_exception('invalid attempt to enrol into frontpage course!');
789         }
791         $name = $this->get_name();
792         $courseid = $instance->courseid;
794         if ($instance->enrol !== $name) {
795             throw new coding_exception('invalid enrol instance!');
796         }
797         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
799         $inserted = false;
800         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
801             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
802                 $ue->timestart    = $timestart;
803                 $ue->timeend      = $timeend;
804                 $ue->modifier     = $USER->id;
805                 $ue->timemodified = time();
806                 $DB->update_record('user_enrolments', $ue);
807             }
808         } else {
809             $ue = new object();
810             $ue->enrolid      = $instance->id;
811             $ue->status       = ENROL_USER_ACTIVE;
812             $ue->userid       = $userid;
813             $ue->timestart    = $timestart;
814             $ue->timeend      = $timeend;
815             $ue->modifier     = $USER->id;
816             $ue->timemodified = time();
817             $ue->id = $DB->insert_record('user_enrolments', $ue);
819             $inserted = true;
820         }
822         if ($roleid) {
823             if ($this->roles_protected()) {
824                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
825             } else {
826                 role_assign($roleid, $userid, $context->id);
827             }
828         }
830         if ($inserted) {
831             // add extra info and trigger event
832             $ue->courseid  = $courseid;
833             $ue->enrol     = $name;
834             events_trigger('user_enrolled', $ue);
835         }
837         // reset primitive require_login() caching
838         if ($userid == $USER->id) {
839             if (isset($USER->enrol['enrolled'][$courseid])) {
840                 unset($USER->enrol['enrolled'][$courseid]);
841             }
842             if (isset($USER->enrol['tempguest'][$courseid])) {
843                 unset($USER->enrol['tempguest'][$courseid]);
844                 $USER->access = remove_temp_roles($context, $USER->access);
845             }
846         }
847     }
849     /**
850      * Store user_enrolments changes and trigger event.
851      *
852      * @param object $ue
853      * @param int $user id
854      * @param int $status
855      * @param int $timestart
856      * @param int $timeend
857      * @return void
858      */
859     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
860         global $DB, $USER;
862         $name = $this->get_name();
864         if ($instance->enrol !== $name) {
865             throw new coding_exception('invalid enrol instance!');
866         }
868         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
869             // weird, user not enrolled
870             return;
871         }
873         $modified = false;
874         if (isset($status) and $ue->status != $status) {
875             $ue->status = $status;
876             $modified = true;
877         }
878         if (isset($timestart) and $ue->timestart != $timestart) {
879             $ue->timestart = $timestart;
880             $modified = true;
881         }
882         if (isset($timeend) and $ue->timeend != $timeend) {
883             $ue->timeend = $timeend;
884             $modified = true;
885         }
887         if (!$modified) {
888             // no change
889             return;
890         }
892         $ue->modifierid = $USER->id;
893         $DB->update_record('user_enrolments', $ue);
895         // trigger event
896         $ue->courseid  = $instance->courseid;
897         $ue->enrol     = $instance->name;
898         events_trigger('user_unenrol_modified', $ue);
899     }
901     /**
902      * Unenrol user from course,
903      * the last unenrolment removes all remaining roles.
904      *
905      * @param stdClass $instance
906      * @param int $userid
907      * @return void
908      */
909     public function unenrol_user(stdClass $instance, $userid) {
910         global $CFG, $USER, $DB;
912         $name = $this->get_name();
913         $courseid = $instance->courseid;
915         if ($instance->enrol !== $name) {
916             throw new coding_exception('invalid enrol instance!');
917         }
918         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
920         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
921             // weird, user not enrolled
922             return;
923         }
925         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
926         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
928         // add extra info and trigger event
929         $ue->courseid  = $courseid;
930         $ue->enrol     = $name;
932         $sql = "SELECT 'x'
933                   FROM {user_enrolments} ue
934                   JOIN {enrol} e ON (e.id = ue.enrolid)
935                   WHERE ue.userid = :userid AND e.courseid = :courseid";
936         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
937             $ue->lastenrol = false;
938             events_trigger('user_unenrolled', $ue);
939             // user still has some enrolments, no big cleanup yet
940         } else {
941             // the big cleanup IS necessary!
943             require_once("$CFG->dirroot/group/lib.php");
944             require_once("$CFG->libdir/gradelib.php");
946             // remove all remaining roles
947             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
949             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
950             groups_delete_group_members($courseid, $userid);
952             grade_user_unenrol($courseid, $userid);
954             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
956             $ue->lastenrol = false;
957             events_trigger('user_unenrolled', $ue);
958         }
959         // reset primitive require_login() caching
960         if ($userid == $USER->id) {
961             if (isset($USER->enrol['enrolled'][$courseid])) {
962                 unset($USER->enrol['enrolled'][$courseid]);
963             }
964             if (isset($USER->enrol['tempguest'][$courseid])) {
965                 unset($USER->enrol['tempguest'][$courseid]);
966                 $USER->access = remove_temp_roles($context, $USER->access);
967             }
968         }
969     }
971     /**
972      * Forces synchronisation of user enrolments.
973      *
974      * This is important especially for external enrol plugins,
975      * this function is called for all enabled enrol plugins
976      * right after every user login.
977      *
978      * @param object $user user record
979      * @return void
980      */
981     public function sync_user_enrolments($user) {
982         // override if necessary
983     }
985     /**
986      * Returns link to page which may be used to add new instance of enrolment plugin in course.
987      * @param int $courseid
988      * @return moodle_url page url
989      */
990     public function get_candidate_link($courseid) {
991         // override for most plugins, check if instance already exists in cases only one instance is supported
992         return NULL;
993     }
995     /**
996      * Is it possible to delete enrol instance via standard UI?
997      *
998      * @param object $instance
999      * @return bool
1000      */
1001     public function instance_deleteable($instance) {
1002         return true;
1003     }
1005     /**
1006      * Returns link to manual enrol UI if exists.
1007      * Does the access control tests automatically.
1008      *
1009      * @param object $instance
1010      * @return moodle_url
1011      */
1012     public function get_manual_enrol_link($instance) {
1013         return NULL;
1014     }
1016     /**
1017      * Returns list of unenrol links for all enrol instances in course.
1018      *
1019      * @param int $courseid
1020      * @return moodle_url
1021      */
1022     public function get_unenrolself_link($instance) {
1023         global $USER, $CFG, $DB;
1025         $name = $this->get_name();
1026         if ($instance->enrol !== $name) {
1027             throw new coding_exception('invalid enrol instance!');
1028         }
1030         if ($instance->courseid == SITEID) {
1031             return NULL;
1032         }
1034         if (!enrol_is_enabled($name)) {
1035             return NULL;
1036         }
1038         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1039             return NULL;
1040         }
1042         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1043         $courseid = $instance->courseid;
1045         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1046             return NULL;
1047         }
1049         $context = get_context_instance(CONTEXT_COURSE, $courseid);
1050         if (!has_capability("enrol/$name:unenrolself", $context)) {
1051             return NULL;
1052         }
1054         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1055             return NULL;
1056         }
1058         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1059     }
1061     /**
1062      * Adds enrol instance UI to course edit form
1063      *
1064      * @param object $instance enrol instance or null if does not exist yet
1065      * @param MoodleQuickForm $mform
1066      * @param object $data
1067      * @param object $context context of existing course or parent category if course does not exist
1068      * @return void
1069      */
1070     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1071         // override - usually at least enable/disable switch, has to add own form header
1072     }
1074     /**
1075      * Validates course edit form data
1076      *
1077      * @param object $instance enrol instance or null if does not exist yet
1078      * @param array $data
1079      * @param object $context context of existing course or parent category if course does not exist
1080      * @return array errors array
1081      */
1082     public function course_edit_validation($instance, array $data, $context) {
1083         return array();
1084     }
1086     /**
1087      * Called after updating/inserting course.
1088      *
1089      * @param bool $inserted true if course just inserted
1090      * @param object $course
1091      * @param object $data form data
1092      * @return void
1093      */
1094     public function course_updated($inserted, $course, $data) {
1095         // override if settings on course edit page or some automatic sync needed
1096     }
1098     /**
1099      * Add new instance of enrol plugin settings.
1100      * @param object $course
1101      * @param array instance fields
1102      * @return int id of new instance
1103      */
1104     public function add_instance($course, array $fields = NULL) {
1105         global $DB;
1107         if ($course->id == SITEID) {
1108             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1109         }
1111         $instance = new object();
1112         $instance->enrol          = $this->get_name();
1113         $instance->status         = ENROL_INSTANCE_ENABLED;
1114         $instance->courseid       = $course->id;
1115         $instance->enrolstartdate = 0;
1116         $instance->enrolenddate   = 0;
1117         $instance->timemodified   = time();
1118         $instance->timecreated    = $instance->timemodified;
1119         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1121         $fields = (array)$fields;
1122         unset($fields['enrol']);
1123         unset($fields['courseid']);
1124         unset($fields['sortorder']);
1125         foreach($fields as $field=>$value) {
1126             $instance->$field = $value;
1127         }
1129         return $DB->insert_record('enrol', $instance);
1130     }
1132     /**
1133      * Add new instance of enrol plugin with default settings,
1134      * called when adding new instance manually or when adding new course.
1135      *
1136      * Not all plugins support this.
1137      *
1138      * @param object $course
1139      * @return int id of new instance or null if no default supported
1140      */
1141     public function add_default_instance($course) {
1142         return null;
1143     }
1145     /**
1146      * Delete course enrol plugin instance, unenrol all users.
1147      * @param object $instance
1148      * @return void
1149      */
1150     public function delete_instance($instance) {
1151         global $DB;
1153         $name = $this->get_name();
1154         if ($instance->enrol !== $name) {
1155             throw new coding_exception('invalid enrol instance!');
1156         }
1158         //first unenrol all users
1159         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1160         foreach ($participants as $participant) {
1161             $this->unenrol_user($instance, $participant->userid);
1162         }
1163         $participants->close();
1165         // now clean up all remainders that were not removed correctly
1166         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1167         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1169         // finally drop the enrol row
1170         $DB->delete_records('enrol', array('id'=>$instance->id));
1171     }
1173     /**
1174      * Creates course enrol form, checks if form submitted
1175      * and enrols user if necessary. It can also redirect.
1176      *
1177      * @param stdClass $instance
1178      * @return string html text, usually a form in a text box
1179      */
1180     public function enrol_page_hook(stdClass $instance) {
1181         return null;
1182     }
1184     /**
1185      * Adds navigation links into course admin block.
1186      *
1187      * By defaults looks for manage links only.
1188      *
1189      * @param navigation_node $instancesnode
1190      * @param object $instance
1191      * @return moodle_url;
1192      */
1193     public function add_course_navigation($instancesnode, stdClass $instance) {
1194         if ($managelink = $this->get_manage_link($instance)) {
1195             $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
1196         }
1197     }
1199     /**
1200      * Returns enrolment instance manage link.
1201      *
1202      * By defaults looks for manage.php file and tests for manage capability.
1203      *
1204      * @param object $instance
1205      * @return moodle_url;
1206      */
1207     public function get_manage_link($instance) {
1208         global $CFG, $DB;
1210         $name = $this->get_name();
1212         if ($instance->enrol !== $name) {
1213              throw new coding_exception('Invalid enrol instance type!');
1214         }
1216         if (!file_exists("$CFG->dirroot/enrol/$name/manage.php")) {
1217             return NULL;
1218         }
1220         if ($instance->courseid == SITEID) {
1221             // no enrolments on the frontpage, only roles there allowed
1222             return NULL;
1223         }
1225         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
1226         if (!has_capability('enrol/'.$name.':manage', $context)) {
1227             return NULL;
1228         }
1230         return new moodle_url("/enrol/$name/manage.php", array('enrolid'=>$instance->id));
1231     }
1233     /**
1234      * Reads version.php and determines if it is necessary
1235      * to execute the cron job now.
1236      * @return bool
1237      */
1238     public function is_cron_required() {
1239         global $CFG;
1241         $name = $this->get_name();
1242         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1243         $plugin = new object();
1244         include($versionfile);
1245         if (empty($plugin->cron)) {
1246             return false;
1247         }
1248         $lastexecuted = $this->get_config('lastcron', 0);
1249         if ($lastexecuted + $plugin->cron < time()) {
1250             return true;
1251         } else {
1252             return false;
1253         }
1254     }
1256     /**
1257      * Called for all enabled enrol plugins that returned true from is_cron_required().
1258      * @return void
1259      */
1260     public function cron() {
1261     }
1263     /**
1264      * Called when user is about to be deleted
1265      * @param object $user
1266      * @return void
1267      */
1268     public function user_delete($user) {
1269         global $DB;
1271         $sql = "SELECT e.*
1272                   FROM {enrol} e
1273                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1274                  WHERE e.enrol = :meta AND ue.userid = :userid";
1275         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1277         $rs = $DB->get_records_recordset($sql, $params);
1278         foreach($rs as $instance) {
1279             $this->unenrol_user($instance, $user->id);
1280         }
1281         $rs->close();
1282     }