MDL-24322 grrr, fixing my previous commit, sorry Eloy
[moodle.git] / lib / enrollib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * This library includes the basic parts of enrol api.
20  * It is available on each page.
21  *
22  * @package    core
23  * @subpackage enrol
24  * @copyright  2010 Petr Skoda {@link http://skodak.org}
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 defined('MOODLE_INTERNAL') || die();
30 /** Course enrol instance enabled. (used in enrol->status) */
31 define('ENROL_INSTANCE_ENABLED', 0);
33 /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
34 define('ENROL_INSTANCE_DISABLED', 1);
36 /** User is active participant (used in user_enrolments->status)*/
37 define('ENROL_USER_ACTIVE', 0);
39 /** User participation in course is suspended (used in user_enrolments->status) */
40 define('ENROL_USER_SUSPENDED', 1);
42 /** Enrol info is cached for this number of seconds in require_login() */
43 define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
45 /** When user disappears from external source, the enrolment is completely removed */
46 define('ENROL_EXT_REMOVED_UNENROL', 0);
48 /** When user disappears from external source, the enrolment is kept as is - one way sync */
49 define('ENROL_EXT_REMOVED_KEEP', 1);
51 /**
52  * When user disappears from external source, user enrolment is suspended, roles are kept as is.
53  * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
54  * assignments, etc.
55  */
56 define('ENROL_EXT_REMOVED_SUSPEND', 2);
58 /**
59  * When user disappears from external source, the enrolment is suspended and roles assigned
60  * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
61  * */
62 define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
64 /**
65  * Returns instances of enrol plugins
66  * @param bool $enable return enabled only
67  * @return array of enrol plugins name=>instance
68  */
69 function enrol_get_plugins($enabled) {
70     global $CFG;
72     $result = array();
74     if ($enabled) {
75         // sorted by enabled plugin order
76         $enabled = explode(',', $CFG->enrol_plugins_enabled);
77         $plugins = array();
78         foreach ($enabled as $plugin) {
79             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
80         }
81     } else {
82         // sorted alphabetically
83         $plugins = get_plugin_list('enrol');
84         ksort($plugins);
85     }
87     foreach ($plugins as $plugin=>$location) {
88         if (!file_exists("$location/lib.php")) {
89             continue;
90         }
91         include_once("$location/lib.php");
92         $class = "enrol_{$plugin}_plugin";
93         if (!class_exists($class)) {
94             continue;
95         }
97         $result[$plugin] = new $class();
98     }
100     return $result;
103 /**
104  * Returns instance of enrol plugin
105  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
106  * @return enrol_plugin
107  */
108 function enrol_get_plugin($name) {
109     global $CFG;
111     if ($name !== clean_param($name, PARAM_SAFEDIR)) {
112         // ignore malformed plugin names completely
113         return null;
114     }
116     $location = "$CFG->dirroot/enrol/$name";
118     if (!file_exists("$location/lib.php")) {
119         return null;
120     }
121     include_once("$location/lib.php");
122     $class = "enrol_{$name}_plugin";
123     if (!class_exists($class)) {
124         return null;
125     }
127     return new $class();
130 /**
131  * Returns enrolment instances in given course.
132  * @param int $courseid
133  * @param bool $enabled
134  * @return array of enrol instances
135  */
136 function enrol_get_instances($courseid, $enabled) {
137     global $DB, $CFG;
139     if (!$enabled) {
140         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
141     }
143     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
145     $enabled = explode(',', $CFG->enrol_plugins_enabled);
146     foreach ($result as $key=>$instance) {
147         if (!in_array($instance->enrol, $enabled)) {
148             unset($result[$key]);
149             continue;
150         }
151         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
152             // broken plugin
153             unset($result[$key]);
154             continue;
155         }
156     }
158     return $result;
161 /**
162  * Checks if a given plugin is in the list of enabled enrolment plugins.
163  *
164  * @param string $enrol Enrolment plugin name
165  * @return boolean Whether the plugin is enabled
166  */
167 function enrol_is_enabled($enrol) {
168     global $CFG;
170     if (empty($CFG->enrol_plugins_enabled)) {
171         return false;
172     }
173     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
176 /**
177  * Check all the login enrolment information for the given user object
178  * by querying the enrolment plugins
179  *
180  * @param object $user
181  * @return void
182  */
183 function enrol_check_plugins($user) {
184     global $CFG;
186     if (empty($user->id) or isguestuser($user)) {
187         // shortcut - there is no enrolment work for guests and not-logged-in users
188         return;
189     }
191     if (is_siteadmin()) {
192         // no sync for admin user, please use admin accounts only for admin tasks like the unix root user!
193         // if plugin fails on sync admins need to be able to log in
194         return;
195     }
197     static $inprogress = array();  // To prevent this function being called more than once in an invocation
199     if (!empty($inprogress[$user->id])) {
200         return;
201     }
203     $inprogress[$user->id] = true;  // Set the flag
205     $enabled = enrol_get_plugins(true);
207     foreach($enabled as $enrol) {
208         $enrol->sync_user_enrolments($user);
209     }
211     unset($inprogress[$user->id]);  // Unset the flag
214 /**
215  * This function adds necessary enrol plugins UI into the course edit form.
216  *
217  * @param MoodleQuickForm $mform
218  * @param object $data course edit form data
219  * @param object $context context of existing course or parent category if course does not exist
220  * @return void
221  */
222 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
223     $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             $plugin->course_edit_form($instance, $mform, $data, $context);
232         }
233     } else {
234         foreach ($plugins as $plugin) {
235             $plugin->course_edit_form(NULL, $mform, $data, $context);
236         }
237     }
240 /**
241  * Validate course edit form data
242  *
243  * @param array $data raw form data
244  * @param object $context context of existing course or parent category if course does not exist
245  * @return array errors array
246  */
247 function enrol_course_edit_validation(array $data, $context) {
248     $errors = array();
249     $plugins = enrol_get_plugins(true);
251     if (!empty($data['id'])) {
252         $instances = enrol_get_instances($data['id'], false);
253         foreach ($instances as $instance) {
254             if (!isset($plugins[$instance->enrol])) {
255                 continue;
256             }
257             $plugin = $plugins[$instance->enrol];
258             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
259         }
260     } else {
261         foreach ($plugins as $plugin) {
262             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
263         }
264     }
266     return $errors;
269 /**
270  * Update enrol instances after course edit form submission
271  * @param bool $inserted true means new course added, false course already existed
272  * @param object $course
273  * @param object $data form data
274  * @return void
275  */
276 function enrol_course_updated($inserted, $course, $data) {
277     global $DB, $CFG;
279     $plugins = enrol_get_plugins(true);
281     foreach ($plugins as $plugin) {
282         $plugin->course_updated($inserted, $course, $data);
283     }
286 /**
287  * Add navigation nodes
288  * @param navigation_node $coursenode
289  * @param object $course
290  * @return void
291  */
292 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
293     global $CFG;
295     $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
297     $instances = enrol_get_instances($course->id, true);
298     $plugins   = enrol_get_plugins(true);
300     // we do not want to break all course pages if there is some borked enrol plugin, right?
301     foreach ($instances as $k=>$instance) {
302         if (!isset($plugins[$instance->enrol])) {
303             unset($instances[$k]);
304         }
305     }
307     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
309     if ($course->id != SITEID) {
310         // list all participants - allows assigning roles, groups, etc.
311         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
312             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
313             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/users', ''));
314         }
316         // manage enrol plugin instances
317         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
318             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
319         } else {
320             $url = NULL;
321         }
322         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
324         // each instance decides how to configure itself or how many other nav items are exposed
325         foreach ($instances as $instance) {
326             if (!isset($plugins[$instance->enrol])) {
327                 continue;
328             }
329             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
330         }
332         if (!$url) {
333             $instancesnode->trim_if_empty();
334         }
335     }
337     // Manage groups in this course or even frontpage
338     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
339         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
340         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
341     }
343      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
344         // Override roles
345         if (has_capability('moodle/role:review', $coursecontext)) {
346             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
347         } else {
348             $url = NULL;
349         }
350         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
352         // Add assign or override roles if allowed
353         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
354             if (has_capability('moodle/role:assign', $coursecontext)) {
355                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
356                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/roles', ''));
357             }
358         }
359         // Check role permissions
360         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
361             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
362             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
363         }
364      }
366      // Deal somehow with users that are not enrolled but still got a role somehow
367     if ($course->id != SITEID) {
368         //TODO, create some new UI for role assignments at course level
369         if (has_capability('moodle/role:assign', $coursecontext)) {
370             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
371             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/roles', ''));
372         }
373     }
375     // just in case nothing was actually added
376     $usersnode->trim_if_empty();
378     if ($course->id != SITEID) {
379         // Unenrol link
380         if (is_enrolled($coursecontext)) {
381             foreach ($instances as $instance) {
382                 if (!isset($plugins[$instance->enrol])) {
383                     continue;
384                 }
385                 $plugin = $plugins[$instance->enrol];
386                 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
387                     $coursenode->add(get_string('unenrolme', 'core_enrol', format_string($course->shortname)), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
388                     break;
389                     //TODO. deal with multiple unenrol links - not likely case, but still...
390                 }
391             }
392         } else {
393             if (is_viewing($coursecontext)) {
394                 // better not show any enrol link, this is intended for managers and inspectors
395             } else {
396                 foreach ($instances as $instance) {
397                     if (!isset($plugins[$instance->enrol])) {
398                         continue;
399                     }
400                     $plugin = $plugins[$instance->enrol];
401                     if ($plugin->show_enrolme_link($instance)) {
402                         $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
403                         $coursenode->add(get_string('enrolme', 'core_enrol', format_string($course->shortname)), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
404                         break;
405                     }
406                 }
407             }
408         }
409     }
412 /**
413  * Returns list of courses current $USER is enrolled in and can access
414  *
415  * - $fields is an array of field names to ADD
416  *   so name the fields you really need, which will
417  *   be added and uniq'd
418  *
419  * @param string|array $fields
420  * @param string $sort
421  * @param int $limit max number of courses
422  * @return array
423  */
424 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
425     global $DB, $USER;
427     // Guest account does not have any courses
428     if (isguestuser() or !isloggedin()) {
429         return(array());
430     }
432     $basefields = array('id', 'category', 'sortorder',
433                         'shortname', 'fullname', 'idnumber',
434                         'startdate', 'visible',
435                         'groupmode', 'groupmodeforce');
437     if (empty($fields)) {
438         $fields = $basefields;
439     } else if (is_string($fields)) {
440         // turn the fields from a string to an array
441         $fields = explode(',', $fields);
442         $fields = array_map('trim', $fields);
443         $fields = array_unique(array_merge($basefields, $fields));
444     } else if (is_array($fields)) {
445         $fields = array_unique(array_merge($basefields, $fields));
446     } else {
447         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
448     }
449     if (in_array('*', $fields)) {
450         $fields = array('*');
451     }
453     $orderby = "";
454     $sort    = trim($sort);
455     if (!empty($sort)) {
456         $rawsorts = explode(',', $sort);
457         $sorts = array();
458         foreach ($rawsorts as $rawsort) {
459             $rawsort = trim($rawsort);
460             if (strpos($rawsort, 'c.') === 0) {
461                 $rawsort = substr($rawsort, 2);
462             }
463             $sorts[] = trim($rawsort);
464         }
465         $sort = 'c.'.implode(',c.', $sorts);
466         $orderby = "ORDER BY $sort";
467     }
469     $wheres = array("c.id <> :siteid");
470     $params = array('siteid'=>SITEID);
472     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
473         // list _only_ this course - anything else is asking for trouble...
474         $wheres[] = "courseid = :loginas";
475         $params['loginas'] = $USER->loginascontext->instanceid;
476     }
478     $coursefields = 'c.' .join(',c.', $fields);
479     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
480     $wheres = implode(" AND ", $wheres);
482     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
483     $sql = "SELECT $coursefields $ccselect
484               FROM {course} c
485               JOIN (SELECT DISTINCT e.courseid
486                       FROM {enrol} e
487                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
488                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
489                    ) en ON (en.courseid = c.id)
490            $ccjoin
491              WHERE $wheres
492           $orderby";
493     $params['userid']  = $USER->id;
494     $params['active']  = ENROL_USER_ACTIVE;
495     $params['enabled'] = ENROL_INSTANCE_ENABLED;
496     $params['now1']    = round(time(), -2); // improves db caching
497     $params['now2']    = $params['now1'];
499     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
501     // preload contexts and check visibility
502     foreach ($courses as $id=>$course) {
503         context_instance_preload($course);
504         if (!$course->visible) {
505             if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
506                 unset($courses[$id]);
507                 continue;
508             }
509             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
510                 unset($courses[$id]);
511                 continue;
512             }
513         }
514         $courses[$id] = $course;
515     }
517     //wow! Is that really all? :-D
519     return $courses;
522 /**
523  * Returns course enrolment information icons.
524  *
525  * @param object $course
526  * @param array $instances enrol instances of this course, improves performance
527  * @return array of pix_icon
528  */
529 function enrol_get_course_info_icons($course, array $instances = NULL) {
530     $icons = array();
531     if (is_null($instances)) {
532         $instances = enrol_get_instances($course->id, true);
533     }
534     $plugins = enrol_get_plugins(true);
535     foreach ($plugins as $name => $plugin) {
536         $pis = array();
537         foreach ($instances as $instance) {
538             if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
539                 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
540                 continue;
541             }
542             if ($instance->enrol == $name) {
543                 $pis[$instance->id] = $instance;
544             }
545         }
546         if ($pis) {
547             $icons = array_merge($icons, $plugin->get_info_icons($pis));
548         }
549     }
550     return $icons;
553 /**
554  * Returns course enrolment detailed information.
555  *
556  * @param object $course
557  * @return array of html fragments - can be used to construct lists
558  */
559 function enrol_get_course_description_texts($course) {
560     $lines = array();
561     $instances = enrol_get_instances($course->id, true);
562     $plugins = enrol_get_plugins(true);
563     foreach ($instances as $instance) {
564         if (!isset($plugins[$instance->enrol])) {
565             //weird
566             continue;
567         }
568         $plugin = $plugins[$instance->enrol];
569         $text = $plugin->get_description_text($instance);
570         if ($text !== NULL) {
571             $lines[] = $text;
572         }
573     }
574     return $lines;
577 /**
578  * Returns list of courses user is enrolled into.
579  *
580  * - $fields is an array of fieldnames to ADD
581  *   so name the fields you really need, which will
582  *   be added and uniq'd
583  *
584  * @param int $userid
585  * @param bool $onlyactive return only active enrolments in courses user may see
586  * @param string|array $fields
587  * @param string $sort
588  * @return array
589  */
590 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
591     global $DB;
593     // Guest account does not have any courses
594     if (isguestuser($userid) or empty($userid)) {
595         return(array());
596     }
598     $basefields = array('id', 'category', 'sortorder',
599                         'shortname', 'fullname', 'idnumber',
600                         'startdate', 'visible',
601                         'groupmode', 'groupmodeforce');
603     if (empty($fields)) {
604         $fields = $basefields;
605     } else if (is_string($fields)) {
606         // turn the fields from a string to an array
607         $fields = explode(',', $fields);
608         $fields = array_map('trim', $fields);
609         $fields = array_unique(array_merge($basefields, $fields));
610     } else if (is_array($fields)) {
611         $fields = array_unique(array_merge($basefields, $fields));
612     } else {
613         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
614     }
615     if (in_array('*', $fields)) {
616         $fields = array('*');
617     }
619     $orderby = "";
620     $sort    = trim($sort);
621     if (!empty($sort)) {
622         $rawsorts = explode(',', $sort);
623         $sorts = array();
624         foreach ($rawsorts as $rawsort) {
625             $rawsort = trim($rawsort);
626             if (strpos($rawsort, 'c.') === 0) {
627                 $rawsort = substr($rawsort, 2);
628             }
629             $sorts[] = trim($rawsort);
630         }
631         $sort = 'c.'.implode(',c.', $sorts);
632         $orderby = "ORDER BY $sort";
633     }
635     $params = array('siteid'=>SITEID);
637     if ($onlyactive) {
638         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
639         $params['now1']    = round(time(), -2); // improves db caching
640         $params['now2']    = $params['now1'];
641         $params['active']  = ENROL_USER_ACTIVE;
642         $params['enabled'] = ENROL_INSTANCE_ENABLED;
643     } else {
644         $subwhere = "";
645     }
647     $coursefields = 'c.' .join(',c.', $fields);
648     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
650     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
651     $sql = "SELECT $coursefields $ccselect
652               FROM {course} c
653               JOIN (SELECT DISTINCT e.courseid
654                       FROM {enrol} e
655                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
656                  $subwhere
657                    ) en ON (en.courseid = c.id)
658            $ccjoin
659              WHERE c.id <> :siteid
660           $orderby";
661     $params['userid']  = $userid;
663     $courses = $DB->get_records_sql($sql, $params);
665     // preload contexts and check visibility
666     foreach ($courses as $id=>$course) {
667         context_instance_preload($course);
668         if ($onlyactive) {
669             if (!$course->visible) {
670                 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
671                     unset($courses[$id]);
672                     continue;
673                 }
674                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
675                     unset($courses[$id]);
676                     continue;
677                 }
678             }
679         }
680         $courses[$id] = $course;
681     }
683     //wow! Is that really all? :-D
685     return $courses;
689 /**
690  * Called when user is about to be deleted.
691  * @param object $user
692  * @return void
693  */
694 function enrol_user_delete($user) {
695     global $DB;
697     $plugins = enrol_get_plugins(true);
698     foreach ($plugins as $plugin) {
699         $plugin->user_delete($user);
700     }
702     // force cleanup of all broken enrolments
703     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
706 /**
707  * Called when course is about to be deleted.
708  * @param stdClass $object
709  * @return void
710  */
711 function enrol_course_delete($course) {
712     global $DB;
714     $instances = enrol_get_instances($course->id, false);
715     $plugins = enrol_get_plugins(true);
716     foreach ($instances as $instance) {
717         if (isset($plugins[$instance->enrol])) {
718             $plugins[$instance->enrol]->delete_instance($instance);
719         }
720         // low level delete in case plugin did not do it
721         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
722         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
723         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
724         $DB->delete_records('enrol', array('id'=>$instance->id));
725     }
728 /**
729  * Try to enrol user via default internal auth plugin.
730  *
731  * For now this is always using the manual enrol plugin...
732  *
733  * @param $courseid
734  * @param $userid
735  * @param $roleid
736  * @param $timestart
737  * @param $timeend
738  * @return bool success
739  */
740 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
741     global $DB;
743     //note: this is hardcoded to manual plugin for now
745     if (!enrol_is_enabled('manual')) {
746         return false;
747     }
749     if (!$enrol = enrol_get_plugin('manual')) {
750         return false;
751     }
752     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
753         return false;
754     }
755     $instance = reset($instances);
757     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
759     return true;
762 /**
763  * All enrol plugins should be based on this class,
764  * this is also the main source of documentation.
765  */
766 abstract class enrol_plugin {
767     protected $config = null;
769     /**
770      * Returns name of this enrol plugin
771      * @return string
772      */
773     public function get_name() {
774         // second word in class is always enrol name, sorry, no fancy plugin names with _
775         $words = explode('_', get_class($this));
776         return $words[1];
777     }
779     /**
780      * Returns localised name of enrol instance
781      *
782      * @param object $instance (null is accepted too)
783      * @return string
784      */
785     public function get_instance_name($instance) {
786         if (empty($instance->name)) {
787             $enrol = $this->get_name();
788             return get_string('pluginname', 'enrol_'.$enrol);
789         } else {
790             $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
791             return format_string($instance->name, true, array('context'=>$context));
792         }
793     }
795     /**
796      * Returns optional enrolment information icons.
797      *
798      * This is used in course list for quick overview of enrolment options.
799      *
800      * We are not using single instance parameter because sometimes
801      * we might want to prevent icon repetition when multiple instances
802      * of one type exist. One instance may also produce several icons.
803      *
804      * @param array $instances all enrol instances of this type in one course
805      * @return array of pix_icon
806      */
807     public function get_info_icons(array $instances) {
808         return array();
809     }
811     /**
812      * Returns optional enrolment instance description text.
813      *
814      * This is used in detailed course information.
815      *
816      *
817      * @param object $instance
818      * @return string short html text
819      */
820     public function get_description_text($instance) {
821         return null;
822     }
824     /**
825      * Makes sure config is loaded and cached.
826      * @return void
827      */
828     protected function load_config() {
829         if (!isset($this->config)) {
830             $name = $this->get_name();
831             if (!$config = get_config("enrol_$name")) {
832                 $config = new stdClass();
833             }
834             $this->config = $config;
835         }
836     }
838     /**
839      * Returns plugin config value
840      * @param  string $name
841      * @param  string $default value if config does not exist yet
842      * @return string value or default
843      */
844     public function get_config($name, $default = NULL) {
845         $this->load_config();
846         return isset($this->config->$name) ? $this->config->$name : $default;
847     }
849     /**
850      * Sets plugin config value
851      * @param  string $name name of config
852      * @param  string $value string config value, null means delete
853      * @return string value
854      */
855     public function set_config($name, $value) {
856         $pluginname = $this->get_name();
857         $this->load_config();
858         if ($value === NULL) {
859             unset($this->config->$name);
860         } else {
861             $this->config->$name = $value;
862         }
863         set_config($name, $value, "enrol_$pluginname");
864     }
866     /**
867      * Does this plugin assign protected roles are can they be manually removed?
868      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
869      */
870     public function roles_protected() {
871         return true;
872     }
874     /**
875      * Does this plugin allow manual enrolments?
876      *
877      * @param stdClass $instance course enrol instance
878      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
879      *
880      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, trues means nobody may add more enrolments manually
881      */
882     public function allow_enrol(stdClass $instance) {
883         return false;
884     }
886     /**
887      * Does this plugin allow manual unenrolments?
888      *
889      * @param stdClass $instance course enrol instance
890      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
891      *
892      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
893      */
894     public function allow_unenrol(stdClass $instance) {
895         return false;
896     }
898     /**
899      * Does this plugin allow manual changes in user_enrolments table?
900      *
901      * All plugins allowing this must implement 'enrol/xxx:manage' capability
902      *
903      * @param stdClass $instance course enrol instance
904      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
905      */
906     public function allow_manage(stdClass $instance) {
907         return false;
908     }
910     /**
911      * Does this plugin support some way to user to self enrol?
912      *
913      * @param stdClass $instance course enrol instance
914      *
915      * @return bool - true means show "Enrol me in this course" link in course UI
916      */
917     public function show_enrolme_link(stdClass $instance) {
918         return false;
919     }
921     /**
922      * Attempt to automatically enrol current user in course without any interaction,
923      * calling code has to make sure the plugin and instance are active.
924      *
925      * @param stdClass $instance course enrol instance
926      * @param stdClass $user record
927      * @return bool|int false means not enrolled, integer means timeend
928      */
929     public function try_autoenrol(stdClass $instance) {
930         global $USER;
932         return false;
933     }
935     /**
936      * Attempt to automatically gain temporary guest access to course,
937      * calling code has to make sure the plugin and instance are active.
938      *
939      * @param stdClass $instance course enrol instance
940      * @param stdClass $user record
941      * @return bool|int false means no guest access, integer means timeend
942      */
943     public function try_guestaccess(stdClass $instance) {
944         global $USER;
946         return false;
947     }
949     /**
950      * Enrol user into course via enrol instance.
951      *
952      * @param stdClass $instance
953      * @param int $userid
954      * @param int $roleid optional role id
955      * @param int $timestart 0 means unknown
956      * @param int $timeend 0 means forever
957      * @return void
958      */
959     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
960         global $DB, $USER, $CFG; // CFG necessary!!!
962         if ($instance->courseid == SITEID) {
963             throw new coding_exception('invalid attempt to enrol into frontpage course!');
964         }
966         $name = $this->get_name();
967         $courseid = $instance->courseid;
969         if ($instance->enrol !== $name) {
970             throw new coding_exception('invalid enrol instance!');
971         }
972         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
974         $inserted = false;
975         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
976             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
977                 $ue->timestart    = $timestart;
978                 $ue->timeend      = $timeend;
979                 $ue->modifier     = $USER->id;
980                 $ue->timemodified = time();
981                 $DB->update_record('user_enrolments', $ue);
982             }
983         } else {
984             $ue = new stdClass();
985             $ue->enrolid      = $instance->id;
986             $ue->status       = ENROL_USER_ACTIVE;
987             $ue->userid       = $userid;
988             $ue->timestart    = $timestart;
989             $ue->timeend      = $timeend;
990             $ue->modifier     = $USER->id;
991             $ue->timecreated  = time();
992             $ue->timemodified = $ue->timecreated;
993             $ue->id = $DB->insert_record('user_enrolments', $ue);
995             $inserted = true;
996         }
998         if ($roleid) {
999             if ($this->roles_protected()) {
1000                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1001             } else {
1002                 role_assign($roleid, $userid, $context->id);
1003             }
1004         }
1006         if ($inserted) {
1007             // add extra info and trigger event
1008             $ue->courseid  = $courseid;
1009             $ue->enrol     = $name;
1010             events_trigger('user_enrolled', $ue);
1011         }
1013         // reset primitive require_login() caching
1014         if ($userid == $USER->id) {
1015             if (isset($USER->enrol['enrolled'][$courseid])) {
1016                 unset($USER->enrol['enrolled'][$courseid]);
1017             }
1018             if (isset($USER->enrol['tempguest'][$courseid])) {
1019                 unset($USER->enrol['tempguest'][$courseid]);
1020                 $USER->access = remove_temp_roles($context, $USER->access);
1021             }
1022         }
1023     }
1025     /**
1026      * Store user_enrolments changes and trigger event.
1027      *
1028      * @param object $ue
1029      * @param int $user id
1030      * @param int $status
1031      * @param int $timestart
1032      * @param int $timeend
1033      * @return void
1034      */
1035     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1036         global $DB, $USER;
1038         $name = $this->get_name();
1040         if ($instance->enrol !== $name) {
1041             throw new coding_exception('invalid enrol instance!');
1042         }
1044         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1045             // weird, user not enrolled
1046             return;
1047         }
1049         $modified = false;
1050         if (isset($status) and $ue->status != $status) {
1051             $ue->status = $status;
1052             $modified = true;
1053         }
1054         if (isset($timestart) and $ue->timestart != $timestart) {
1055             $ue->timestart = $timestart;
1056             $modified = true;
1057         }
1058         if (isset($timeend) and $ue->timeend != $timeend) {
1059             $ue->timeend = $timeend;
1060             $modified = true;
1061         }
1063         if (!$modified) {
1064             // no change
1065             return;
1066         }
1068         $ue->modifierid = $USER->id;
1069         $DB->update_record('user_enrolments', $ue);
1071         // trigger event
1072         $ue->courseid  = $instance->courseid;
1073         $ue->enrol     = $instance->name;
1074         events_trigger('user_unenrol_modified', $ue);
1075     }
1077     /**
1078      * Unenrol user from course,
1079      * the last unenrolment removes all remaining roles.
1080      *
1081      * @param stdClass $instance
1082      * @param int $userid
1083      * @return void
1084      */
1085     public function unenrol_user(stdClass $instance, $userid) {
1086         global $CFG, $USER, $DB;
1088         $name = $this->get_name();
1089         $courseid = $instance->courseid;
1091         if ($instance->enrol !== $name) {
1092             throw new coding_exception('invalid enrol instance!');
1093         }
1094         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1096         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1097             // weird, user not enrolled
1098             return;
1099         }
1101         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1102         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1104         // add extra info and trigger event
1105         $ue->courseid  = $courseid;
1106         $ue->enrol     = $name;
1108         $sql = "SELECT 'x'
1109                   FROM {user_enrolments} ue
1110                   JOIN {enrol} e ON (e.id = ue.enrolid)
1111                   WHERE ue.userid = :userid AND e.courseid = :courseid";
1112         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1113             $ue->lastenrol = false;
1114             events_trigger('user_unenrolled', $ue);
1115             // user still has some enrolments, no big cleanup yet
1116         } else {
1117             // the big cleanup IS necessary!
1119             require_once("$CFG->dirroot/group/lib.php");
1120             require_once("$CFG->libdir/gradelib.php");
1122             // remove all remaining roles
1123             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1125             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1126             groups_delete_group_members($courseid, $userid);
1128             grade_user_unenrol($courseid, $userid);
1130             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1132             $ue->lastenrol = true; // means user not enrolled any more
1133             events_trigger('user_unenrolled', $ue);
1134         }
1135         // reset primitive require_login() caching
1136         if ($userid == $USER->id) {
1137             if (isset($USER->enrol['enrolled'][$courseid])) {
1138                 unset($USER->enrol['enrolled'][$courseid]);
1139             }
1140             if (isset($USER->enrol['tempguest'][$courseid])) {
1141                 unset($USER->enrol['tempguest'][$courseid]);
1142                 $USER->access = remove_temp_roles($context, $USER->access);
1143             }
1144         }
1145     }
1147     /**
1148      * Forces synchronisation of user enrolments.
1149      *
1150      * This is important especially for external enrol plugins,
1151      * this function is called for all enabled enrol plugins
1152      * right after every user login.
1153      *
1154      * @param object $user user record
1155      * @return void
1156      */
1157     public function sync_user_enrolments($user) {
1158         // override if necessary
1159     }
1161     /**
1162      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1163      * @param int $courseid
1164      * @return moodle_url page url
1165      */
1166     public function get_newinstance_link($courseid) {
1167         // override for most plugins, check if instance already exists in cases only one instance is supported
1168         return NULL;
1169     }
1171     /**
1172      * Is it possible to delete enrol instance via standard UI?
1173      *
1174      * @param object $instance
1175      * @return bool
1176      */
1177     public function instance_deleteable($instance) {
1178         return true;
1179     }
1181     /**
1182      * Returns link to manual enrol UI if exists.
1183      * Does the access control tests automatically.
1184      *
1185      * @param object $instance
1186      * @return moodle_url
1187      */
1188     public function get_manual_enrol_link($instance) {
1189         return NULL;
1190     }
1192     /**
1193      * Returns list of unenrol links for all enrol instances in course.
1194      *
1195      * @param int $instance
1196      * @return moodle_url or NULL if self unenrolment not supported
1197      */
1198     public function get_unenrolself_link($instance) {
1199         global $USER, $CFG, $DB;
1201         $name = $this->get_name();
1202         if ($instance->enrol !== $name) {
1203             throw new coding_exception('invalid enrol instance!');
1204         }
1206         if ($instance->courseid == SITEID) {
1207             return NULL;
1208         }
1210         if (!enrol_is_enabled($name)) {
1211             return NULL;
1212         }
1214         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1215             return NULL;
1216         }
1218         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1219             return NULL;
1220         }
1222         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1224         if (!has_capability("enrol/$name:unenrolself", $context)) {
1225             return NULL;
1226         }
1228         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1229             return NULL;
1230         }
1232         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1233     }
1235     /**
1236      * Adds enrol instance UI to course edit form
1237      *
1238      * @param object $instance enrol instance or null if does not exist yet
1239      * @param MoodleQuickForm $mform
1240      * @param object $data
1241      * @param object $context context of existing course or parent category if course does not exist
1242      * @return void
1243      */
1244     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1245         // override - usually at least enable/disable switch, has to add own form header
1246     }
1248     /**
1249      * Validates course edit form data
1250      *
1251      * @param object $instance enrol instance or null if does not exist yet
1252      * @param array $data
1253      * @param object $context context of existing course or parent category if course does not exist
1254      * @return array errors array
1255      */
1256     public function course_edit_validation($instance, array $data, $context) {
1257         return array();
1258     }
1260     /**
1261      * Called after updating/inserting course.
1262      *
1263      * @param bool $inserted true if course just inserted
1264      * @param object $course
1265      * @param object $data form data
1266      * @return void
1267      */
1268     public function course_updated($inserted, $course, $data) {
1269         if ($inserted) {
1270             if ($this->get_config('defaultenrol')) {
1271                 $this->add_default_instance($course);
1272             }
1273         }
1274     }
1276     /**
1277      * Add new instance of enrol plugin.
1278      * @param object $course
1279      * @param array instance fields
1280      * @return int id of new instance, null if can not be created
1281      */
1282     public function add_instance($course, array $fields = NULL) {
1283         global $DB;
1285         if ($course->id == SITEID) {
1286             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1287         }
1289         $instance = new stdClass();
1290         $instance->enrol          = $this->get_name();
1291         $instance->status         = ENROL_INSTANCE_ENABLED;
1292         $instance->courseid       = $course->id;
1293         $instance->enrolstartdate = 0;
1294         $instance->enrolenddate   = 0;
1295         $instance->timemodified   = time();
1296         $instance->timecreated    = $instance->timemodified;
1297         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1299         $fields = (array)$fields;
1300         unset($fields['enrol']);
1301         unset($fields['courseid']);
1302         unset($fields['sortorder']);
1303         foreach($fields as $field=>$value) {
1304             $instance->$field = $value;
1305         }
1307         return $DB->insert_record('enrol', $instance);
1308     }
1310     /**
1311      * Add new instance of enrol plugin with default settings,
1312      * called when adding new instance manually or when adding new course.
1313      *
1314      * Not all plugins support this.
1315      *
1316      * @param object $course
1317      * @return int id of new instance or null if no default supported
1318      */
1319     public function add_default_instance($course) {
1320         return null;
1321     }
1323     /**
1324      * Delete course enrol plugin instance, unenrol all users.
1325      * @param object $instance
1326      * @return void
1327      */
1328     public function delete_instance($instance) {
1329         global $DB;
1331         $name = $this->get_name();
1332         if ($instance->enrol !== $name) {
1333             throw new coding_exception('invalid enrol instance!');
1334         }
1336         //first unenrol all users
1337         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1338         foreach ($participants as $participant) {
1339             $this->unenrol_user($instance, $participant->userid);
1340         }
1341         $participants->close();
1343         // now clean up all remainders that were not removed correctly
1344         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1345         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1347         // finally drop the enrol row
1348         $DB->delete_records('enrol', array('id'=>$instance->id));
1349     }
1351     /**
1352      * Creates course enrol form, checks if form submitted
1353      * and enrols user if necessary. It can also redirect.
1354      *
1355      * @param stdClass $instance
1356      * @return string html text, usually a form in a text box
1357      */
1358     public function enrol_page_hook(stdClass $instance) {
1359         return null;
1360     }
1362     /**
1363      * Adds navigation links into course admin block.
1364      *
1365      * By defaults looks for manage links only.
1366      *
1367      * @param navigation_node $instancesnode
1368      * @param object $instance
1369      * @return void
1370      */
1371     public function add_course_navigation($instancesnode, stdClass $instance) {
1372         // usually adds manage users
1373     }
1375     /**
1376      * Returns edit icons for the page with list of instances
1377      * @param stdClass $instance
1378      * @return array
1379      */
1380     public function get_action_icons(stdClass $instance) {
1381         return array();
1382     }
1384     /**
1385      * Reads version.php and determines if it is necessary
1386      * to execute the cron job now.
1387      * @return bool
1388      */
1389     public function is_cron_required() {
1390         global $CFG;
1392         $name = $this->get_name();
1393         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1394         $plugin = new stdClass();
1395         include($versionfile);
1396         if (empty($plugin->cron)) {
1397             return false;
1398         }
1399         $lastexecuted = $this->get_config('lastcron', 0);
1400         if ($lastexecuted + $plugin->cron < time()) {
1401             return true;
1402         } else {
1403             return false;
1404         }
1405     }
1407     /**
1408      * Called for all enabled enrol plugins that returned true from is_cron_required().
1409      * @return void
1410      */
1411     public function cron() {
1412     }
1414     /**
1415      * Called when user is about to be deleted
1416      * @param object $user
1417      * @return void
1418      */
1419     public function user_delete($user) {
1420         global $DB;
1422         $sql = "SELECT e.*
1423                   FROM {enrol} e
1424                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1425                  WHERE e.enrol = :meta AND ue.userid = :userid";
1426         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1428         $rs = $DB->get_records_recordset($sql, $params);
1429         foreach($rs as $instance) {
1430             $this->unenrol_user($instance, $user->id);
1431         }
1432         $rs->close();
1433     }