MDL-22910 fixing more DISTINCT+text field issues, hopefully this is all from enrol
[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 = implode(" AND ", $wheres);
442     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
443     $sql = "SELECT $coursefields $ccselect
444               FROM {course} c
445               JOIN (SELECT DISTINCT e.courseid
446                       FROM {enrol} e
447                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
448                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
449                    ) en ON (en.courseid = c.id)
450            $ccjoin
451              WHERE $wheres
452           $orderby";
453     $params['userid']  = $USER->id;
454     $params['active']  = ENROL_USER_ACTIVE;
455     $params['enabled'] = ENROL_INSTANCE_ENABLED;
456     $params['now1']    = round(time(), -2); // improves db caching
457     $params['now2']    = $params['now1'];
459     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
461     // preload contexts and check visibility
462     foreach ($courses as $id=>$course) {
463         context_instance_preload($course);
464         if (!$course->visible) {
465             if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
466                 unset($course[$id]);
467                 continue;
468             }
469             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
470                 unset($course[$id]);
471                 continue;
472             }
473         }
474         $courses[$id] = $course;
475     }
477     //wow! Is that really all? :-D
479     return $courses;
482 /**
483  * Returns list of courses user is enrolled into.
484  *
485  * - $fields is an array of fieldnames to ADD
486  *   so name the fields you really need, which will
487  *   be added and uniq'd
488  *
489  * @param int $userid
490  * @param bool $onlyactive return only active enrolments in courses user may see
491  * @param strin|array $fields
492  * @param string $sort
493  * @return array
494  */
495 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
496     global $DB;
498     // Guest account does not have any courses
499     if (isguestuser($userid) or empty($userid)) {
500         return(array());
501     }
503     $basefields = array('id', 'category', 'sortorder',
504                         'shortname', 'fullname', 'idnumber',
505                         'startdate', 'visible',
506                         'groupmode', 'groupmodeforce');
508     if (empty($fields)) {
509         $fields = $basefields;
510     } else if (is_string($fields)) {
511         // turn the fields from a string to an array
512         $fields = explode(',', $fields);
513         $fields = array_map('trim', $fields);
514         $fields = array_unique(array_merge($basefields, $fields));
515     } else if (is_array($fields)) {
516         $fields = array_unique(array_merge($basefields, $fields));
517     } else {
518         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
519     }
520     if (in_array('*', $fields)) {
521         $fields = array('*');
522     }
524     $orderby = "";
525     $sort    = trim($sort);
526     if (!empty($sort)) {
527         $rawsorts = explode(',', $sort);
528         $sorts = array();
529         foreach ($rawsorts as $rawsort) {
530             $rawsort = trim($rawsort);
531             if (strpos($rawsort, 'c.') === 0) {
532                 $rawsort = substr($rawsort, 2);
533             }
534             $sorts[] = trim($rawsort);
535         }
536         $sort = 'c.'.implode(',c.', $sorts);
537         $orderby = "ORDER BY $sort";
538     }
540     $params = array('siteid'=>SITEID);
542     if ($onlyactive) {
543         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND 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     } else {
549         $subwhere = "";
550     }
552     $coursefields = 'c.' .join(',c.', $fields);
553     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
555     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
556     $sql = "SELECT $coursefields $ccselect
557               FROM {course} c
558               JOIN (SELECT DISTINCT e.courseid
559                       FROM {enrol} e
560                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
561                  $subwhere
562                    ) en ON (en.courseid = c.id)
563            $ccjoin
564              WHERE c.id <> :siteid
565           $orderby";
566     $params['userid']  = $userid;
568     $courses = $DB->get_records_sql($sql, $params);
570     // preload contexts and check visibility
571     foreach ($courses as $id=>$course) {
572         context_instance_preload($course);
573         if ($onlyactive) {
574             if (!$course->visible) {
575                 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
576                     unset($course[$id]);
577                     continue;
578                 }
579                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
580                     unset($course[$id]);
581                     continue;
582                 }
583             }
584         }
585         $courses[$id] = $course;
586     }
588     //wow! Is that really all? :-D
590     return $courses;
594 /**
595  * Called when user is about to be deleted.
596  * @param object $user
597  * @return void
598  */
599 function enrol_user_delete($user) {
600     global $DB;
602     $plugins = enrol_get_plugins(true);
603     foreach ($plugins as $plugin) {
604         $plugin->user_delete($user);
605     }
607     // force cleanup of all broken enrolments
608     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
611 /**
612  * Try to enrol user via default internal auth plugin.
613  *
614  * For now this is always using the manual enrol plugin...
615  *
616  * @param $courseid
617  * @param $userid
618  * @param $roleid
619  * @param $timestart
620  * @param $timeend
621  * @return bool success
622  */
623 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
624     global $DB;
626     //note: this is hardcoded to manual plugin for now
628     if (!enrol_is_enabled('manual')) {
629         return false;
630     }
632     if (!$enrol = enrol_get_plugin('manual')) {
633         return false;
634     }
635     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
636         return false;
637     }
638     $instance = reset($instances);
640     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
642     return true;
645 /**
646  * All enrol plugins should be based on this class,
647  * this is also the main source of documentation.
648  */
649 abstract class enrol_plugin {
650     protected $config = null;
652     /**
653      * Returns name of this enrol plugin
654      * @return string
655      */
656     public function get_name() {
657         // second word in class is always enrol name
658         $words = explode('_', get_class($this));
659         return $words[1];
660     }
662     /**
663      * Returns localised name of enrol instance
664      *
665      * @param object $instance (null is accepted too)
666      * @return string
667      */
668     public function get_instance_name($instance) {
669         if (empty($instance->name)) {
670             $enrol = $this->get_name();
671             return get_string('pluginname', 'enrol_'.$enrol);
672         } else {
673             return format_string($instance->name);
674         }
675     }
677     /**
678      * Makes sure config is loaded and cached.
679      * @return void
680      */
681     protected function load_config() {
682         if (!isset($this->config)) {
683             $name = $this->get_name();
684             if (!$config = get_config("enrol_$name")) {
685                 $config = new object();
686             }
687             $this->config = $config;
688         }
689     }
691     /**
692      * Returns plugin config value
693      * @param  string $name
694      * @param  string $default value if config does not exist yet
695      * @return string value or default
696      */
697     public function get_config($name, $default = NULL) {
698         $this->load_config();
699         return isset($this->config->$name) ? $this->config->$name : $default;
700     }
702     /**
703      * Sets plugin config value
704      * @param  string $name name of config
705      * @param  string $value string config value, null means delete
706      * @return string value
707      */
708     public function set_config($name, $value) {
709         $name = $this->get_name();
710         $this->load_config();
711         if ($value === NULL) {
712             unset($this->config->$name);
713         } else {
714             $this->config->$name = $value;
715         }
716         set_config($name, $value, "enrol_$name");
717     }
719     /**
720      * Does this plugin assign protected roles are can they be manually removed?
721      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
722      */
723     public function roles_protected() {
724         return true;
725     }
727     /**
728      * Does this plugin allow manual unenrolments?
729      *
730      * @param stdClass $instance course enrol instance
731      * ALl plugins allowing this must implement 'enrol/xxx:unenrol' capability
732      *
733      * @return bool - true means anybody may unenrol others freely, trues means nobody may touch user_enrolments
734      */
735     public function allow_unenrol(stdClass $instance) {
736         return false;
737     }
739     /**
740      * Does this plugin allow manual changes in user_enrolments table?
741      *
742      * ALl plugins allowing this must implement 'enrol/xxx:manage' capability
743      *
744      * @param stdClass $instance course enrol instance
745      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
746      */
747     public function allow_manage(stdClass $instance) {
748         return false;
749     }
751     /**
752      * Attempt to automatically enrol current user in course without any interaction,
753      * calling code has to make sure the plugin and instance are active.
754      *
755      * @param stdClass $instance course enrol instance
756      * @param stdClass $user record
757      * @return bool|int false means not enrolled, integer means timeend
758      */
759     public function try_autoenrol(stdClass $instance) {
760         global $USER;
762         return false;
763     }
765     /**
766      * Attempt to automatically gain temporary guest access to course,
767      * calling code has to make sure the plugin and instance are active.
768      *
769      * @param stdClass $instance course enrol instance
770      * @param stdClass $user record
771      * @return bool|int false means no guest access, integer means timeend
772      */
773     public function try_guestaccess(stdClass $instance) {
774         global $USER;
776         return false;
777     }
779     /**
780      * Enrol user into course via enrol instance.
781      *
782      * @param stdClass $instance
783      * @param int $userid
784      * @param int $roleid optional role id
785      * @param int $timestart
786      * @param int $timeend
787      * @return void
788      */
789     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
790         global $DB, $USER, $CFG; // CFG necessary!!!
792         if ($instance->courseid == SITEID) {
793             throw new coding_exception('invalid attempt to enrol into frontpage course!');
794         }
796         $name = $this->get_name();
797         $courseid = $instance->courseid;
799         if ($instance->enrol !== $name) {
800             throw new coding_exception('invalid enrol instance!');
801         }
802         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
804         $inserted = false;
805         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
806             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
807                 $ue->timestart    = $timestart;
808                 $ue->timeend      = $timeend;
809                 $ue->modifier     = $USER->id;
810                 $ue->timemodified = time();
811                 $DB->update_record('user_enrolments', $ue);
812             }
813         } else {
814             $ue = new object();
815             $ue->enrolid      = $instance->id;
816             $ue->status       = ENROL_USER_ACTIVE;
817             $ue->userid       = $userid;
818             $ue->timestart    = $timestart;
819             $ue->timeend      = $timeend;
820             $ue->modifier     = $USER->id;
821             $ue->timemodified = time();
822             $ue->id = $DB->insert_record('user_enrolments', $ue);
824             $inserted = true;
825         }
827         if ($roleid) {
828             if ($this->roles_protected()) {
829                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
830             } else {
831                 role_assign($roleid, $userid, $context->id);
832             }
833         }
835         if ($inserted) {
836             // add extra info and trigger event
837             $ue->courseid  = $courseid;
838             $ue->enrol     = $name;
839             events_trigger('user_enrolled', $ue);
840         }
842         // reset primitive require_login() caching
843         if ($userid == $USER->id) {
844             if (isset($USER->enrol['enrolled'][$courseid])) {
845                 unset($USER->enrol['enrolled'][$courseid]);
846             }
847             if (isset($USER->enrol['tempguest'][$courseid])) {
848                 unset($USER->enrol['tempguest'][$courseid]);
849                 $USER->access = remove_temp_roles($context, $USER->access);
850             }
851         }
852     }
854     /**
855      * Store user_enrolments changes and trigger event.
856      *
857      * @param object $ue
858      * @param int $user id
859      * @param int $status
860      * @param int $timestart
861      * @param int $timeend
862      * @return void
863      */
864     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
865         global $DB, $USER;
867         $name = $this->get_name();
869         if ($instance->enrol !== $name) {
870             throw new coding_exception('invalid enrol instance!');
871         }
873         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
874             // weird, user not enrolled
875             return;
876         }
878         $modified = false;
879         if (isset($status) and $ue->status != $status) {
880             $ue->status = $status;
881             $modified = true;
882         }
883         if (isset($timestart) and $ue->timestart != $timestart) {
884             $ue->timestart = $timestart;
885             $modified = true;
886         }
887         if (isset($timeend) and $ue->timeend != $timeend) {
888             $ue->timeend = $timeend;
889             $modified = true;
890         }
892         if (!$modified) {
893             // no change
894             return;
895         }
897         $ue->modifierid = $USER->id;
898         $DB->update_record('user_enrolments', $ue);
900         // trigger event
901         $ue->courseid  = $instance->courseid;
902         $ue->enrol     = $instance->name;
903         events_trigger('user_unenrol_modified', $ue);
904     }
906     /**
907      * Unenrol user from course,
908      * the last unenrolment removes all remaining roles.
909      *
910      * @param stdClass $instance
911      * @param int $userid
912      * @return void
913      */
914     public function unenrol_user(stdClass $instance, $userid) {
915         global $CFG, $USER, $DB;
917         $name = $this->get_name();
918         $courseid = $instance->courseid;
920         if ($instance->enrol !== $name) {
921             throw new coding_exception('invalid enrol instance!');
922         }
923         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
925         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
926             // weird, user not enrolled
927             return;
928         }
930         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
931         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
933         // add extra info and trigger event
934         $ue->courseid  = $courseid;
935         $ue->enrol     = $name;
937         $sql = "SELECT 'x'
938                   FROM {user_enrolments} ue
939                   JOIN {enrol} e ON (e.id = ue.enrolid)
940                   WHERE ue.userid = :userid AND e.courseid = :courseid";
941         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
942             $ue->lastenrol = false;
943             events_trigger('user_unenrolled', $ue);
944             // user still has some enrolments, no big cleanup yet
945         } else {
946             // the big cleanup IS necessary!
948             require_once("$CFG->dirroot/group/lib.php");
949             require_once("$CFG->libdir/gradelib.php");
951             // remove all remaining roles
952             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
954             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
955             groups_delete_group_members($courseid, $userid);
957             grade_user_unenrol($courseid, $userid);
959             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
961             $ue->lastenrol = false;
962             events_trigger('user_unenrolled', $ue);
963         }
964         // reset primitive require_login() caching
965         if ($userid == $USER->id) {
966             if (isset($USER->enrol['enrolled'][$courseid])) {
967                 unset($USER->enrol['enrolled'][$courseid]);
968             }
969             if (isset($USER->enrol['tempguest'][$courseid])) {
970                 unset($USER->enrol['tempguest'][$courseid]);
971                 $USER->access = remove_temp_roles($context, $USER->access);
972             }
973         }
974     }
976     /**
977      * Forces synchronisation of user enrolments.
978      *
979      * This is important especially for external enrol plugins,
980      * this function is called for all enabled enrol plugins
981      * right after every user login.
982      *
983      * @param object $user user record
984      * @return void
985      */
986     public function sync_user_enrolments($user) {
987         // override if necessary
988     }
990     /**
991      * Returns link to page which may be used to add new instance of enrolment plugin in course.
992      * @param int $courseid
993      * @return moodle_url page url
994      */
995     public function get_candidate_link($courseid) {
996         // override for most plugins, check if instance already exists in cases only one instance is supported
997         return NULL;
998     }
1000     /**
1001      * Is it possible to delete enrol instance via standard UI?
1002      *
1003      * @param object $instance
1004      * @return bool
1005      */
1006     public function instance_deleteable($instance) {
1007         return true;
1008     }
1010     /**
1011      * Returns link to manual enrol UI if exists.
1012      * Does the access control tests automatically.
1013      *
1014      * @param object $instance
1015      * @return moodle_url
1016      */
1017     public function get_manual_enrol_link($instance) {
1018         return NULL;
1019     }
1021     /**
1022      * Returns list of unenrol links for all enrol instances in course.
1023      *
1024      * @param int $courseid
1025      * @return moodle_url
1026      */
1027     public function get_unenrolself_link($instance) {
1028         global $USER, $CFG, $DB;
1030         $name = $this->get_name();
1031         if ($instance->enrol !== $name) {
1032             throw new coding_exception('invalid enrol instance!');
1033         }
1035         if ($instance->courseid == SITEID) {
1036             return NULL;
1037         }
1039         if (!enrol_is_enabled($name)) {
1040             return NULL;
1041         }
1043         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1044             return NULL;
1045         }
1047         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1048         $courseid = $instance->courseid;
1050         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1051             return NULL;
1052         }
1054         $context = get_context_instance(CONTEXT_COURSE, $courseid);
1055         if (!has_capability("enrol/$name:unenrolself", $context)) {
1056             return NULL;
1057         }
1059         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1060             return NULL;
1061         }
1063         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1064     }
1066     /**
1067      * Adds enrol instance UI to course edit form
1068      *
1069      * @param object $instance enrol instance or null if does not exist yet
1070      * @param MoodleQuickForm $mform
1071      * @param object $data
1072      * @param object $context context of existing course or parent category if course does not exist
1073      * @return void
1074      */
1075     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1076         // override - usually at least enable/disable switch, has to add own form header
1077     }
1079     /**
1080      * Validates course edit form data
1081      *
1082      * @param object $instance enrol instance or null if does not exist yet
1083      * @param array $data
1084      * @param object $context context of existing course or parent category if course does not exist
1085      * @return array errors array
1086      */
1087     public function course_edit_validation($instance, array $data, $context) {
1088         return array();
1089     }
1091     /**
1092      * Called after updating/inserting course.
1093      *
1094      * @param bool $inserted true if course just inserted
1095      * @param object $course
1096      * @param object $data form data
1097      * @return void
1098      */
1099     public function course_updated($inserted, $course, $data) {
1100         // override if settings on course edit page or some automatic sync needed
1101     }
1103     /**
1104      * Add new instance of enrol plugin settings.
1105      * @param object $course
1106      * @param array instance fields
1107      * @return int id of new instance
1108      */
1109     public function add_instance($course, array $fields = NULL) {
1110         global $DB;
1112         if ($course->id == SITEID) {
1113             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1114         }
1116         $instance = new object();
1117         $instance->enrol          = $this->get_name();
1118         $instance->status         = ENROL_INSTANCE_ENABLED;
1119         $instance->courseid       = $course->id;
1120         $instance->enrolstartdate = 0;
1121         $instance->enrolenddate   = 0;
1122         $instance->timemodified   = time();
1123         $instance->timecreated    = $instance->timemodified;
1124         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1126         $fields = (array)$fields;
1127         unset($fields['enrol']);
1128         unset($fields['courseid']);
1129         unset($fields['sortorder']);
1130         foreach($fields as $field=>$value) {
1131             $instance->$field = $value;
1132         }
1134         return $DB->insert_record('enrol', $instance);
1135     }
1137     /**
1138      * Add new instance of enrol plugin with default settings,
1139      * called when adding new instance manually or when adding new course.
1140      *
1141      * Not all plugins support this.
1142      *
1143      * @param object $course
1144      * @return int id of new instance or null if no default supported
1145      */
1146     public function add_default_instance($course) {
1147         return null;
1148     }
1150     /**
1151      * Delete course enrol plugin instance, unenrol all users.
1152      * @param object $instance
1153      * @return void
1154      */
1155     public function delete_instance($instance) {
1156         global $DB;
1158         $name = $this->get_name();
1159         if ($instance->enrol !== $name) {
1160             throw new coding_exception('invalid enrol instance!');
1161         }
1163         //first unenrol all users
1164         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1165         foreach ($participants as $participant) {
1166             $this->unenrol_user($instance, $participant->userid);
1167         }
1168         $participants->close();
1170         // now clean up all remainders that were not removed correctly
1171         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1172         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1174         // finally drop the enrol row
1175         $DB->delete_records('enrol', array('id'=>$instance->id));
1176     }
1178     /**
1179      * Creates course enrol form, checks if form submitted
1180      * and enrols user if necessary. It can also redirect.
1181      *
1182      * @param stdClass $instance
1183      * @return string html text, usually a form in a text box
1184      */
1185     public function enrol_page_hook(stdClass $instance) {
1186         return null;
1187     }
1189     /**
1190      * Adds navigation links into course admin block.
1191      *
1192      * By defaults looks for manage links only.
1193      *
1194      * @param navigation_node $instancesnode
1195      * @param object $instance
1196      * @return moodle_url;
1197      */
1198     public function add_course_navigation($instancesnode, stdClass $instance) {
1199         if ($managelink = $this->get_manage_link($instance)) {
1200             $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
1201         }
1202     }
1204     /**
1205      * Returns enrolment instance manage link.
1206      *
1207      * By defaults looks for manage.php file and tests for manage capability.
1208      *
1209      * @param object $instance
1210      * @return moodle_url;
1211      */
1212     public function get_manage_link($instance) {
1213         global $CFG, $DB;
1215         $name = $this->get_name();
1217         if ($instance->enrol !== $name) {
1218              throw new coding_exception('Invalid enrol instance type!');
1219         }
1221         if (!file_exists("$CFG->dirroot/enrol/$name/manage.php")) {
1222             return NULL;
1223         }
1225         if ($instance->courseid == SITEID) {
1226             // no enrolments on the frontpage, only roles there allowed
1227             return NULL;
1228         }
1230         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
1231         if (!has_capability('enrol/'.$name.':manage', $context)) {
1232             return NULL;
1233         }
1235         return new moodle_url("/enrol/$name/manage.php", array('enrolid'=>$instance->id));
1236     }
1238     /**
1239      * Reads version.php and determines if it is necessary
1240      * to execute the cron job now.
1241      * @return bool
1242      */
1243     public function is_cron_required() {
1244         global $CFG;
1246         $name = $this->get_name();
1247         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1248         $plugin = new object();
1249         include($versionfile);
1250         if (empty($plugin->cron)) {
1251             return false;
1252         }
1253         $lastexecuted = $this->get_config('lastcron', 0);
1254         if ($lastexecuted + $plugin->cron < time()) {
1255             return true;
1256         } else {
1257             return false;
1258         }
1259     }
1261     /**
1262      * Called for all enabled enrol plugins that returned true from is_cron_required().
1263      * @return void
1264      */
1265     public function cron() {
1266     }
1268     /**
1269      * Called when user is about to be deleted
1270      * @param object $user
1271      * @return void
1272      */
1273     public function user_delete($user) {
1274         global $DB;
1276         $sql = "SELECT e.*
1277                   FROM {enrol} e
1278                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1279                  WHERE e.enrol = :meta AND ue.userid = :userid";
1280         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1282         $rs = $DB->get_records_recordset($sql, $params);
1283         foreach($rs as $instance) {
1284             $this->unenrol_user($instance, $user->id);
1285         }
1286         $rs->close();
1287     }