fixed broken enrol description fetching
[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 strin|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 strin|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  * Try to enrol user via default internal auth plugin.
708  *
709  * For now this is always using the manual enrol plugin...
710  *
711  * @param $courseid
712  * @param $userid
713  * @param $roleid
714  * @param $timestart
715  * @param $timeend
716  * @return bool success
717  */
718 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
719     global $DB;
721     //note: this is hardcoded to manual plugin for now
723     if (!enrol_is_enabled('manual')) {
724         return false;
725     }
727     if (!$enrol = enrol_get_plugin('manual')) {
728         return false;
729     }
730     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
731         return false;
732     }
733     $instance = reset($instances);
735     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
737     return true;
740 /**
741  * All enrol plugins should be based on this class,
742  * this is also the main source of documentation.
743  */
744 abstract class enrol_plugin {
745     protected $config = null;
747     /**
748      * Returns name of this enrol plugin
749      * @return string
750      */
751     public function get_name() {
752         // second word in class is always enrol name, sorry, no fancy plugin names with _
753         $words = explode('_', get_class($this));
754         return $words[1];
755     }
757     /**
758      * Returns localised name of enrol instance
759      *
760      * @param object $instance (null is accepted too)
761      * @return string
762      */
763     public function get_instance_name($instance) {
764         if (empty($instance->name)) {
765             $enrol = $this->get_name();
766             return get_string('pluginname', 'enrol_'.$enrol);
767         } else {
768             $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
769             return format_string($instance->name, true, array('context'=>$context));
770         }
771     }
773     /**
774      * Returns optional enrolment information icons.
775      *
776      * This is used in course list for quick overview of enrolment options.
777      *
778      * We are not using single instance parameter because sometimes
779      * we might want to prevent icon repetition when multiple instances
780      * of one type exist. One instance may also produce several icons.
781      *
782      * @param array $instances all enrol instances of this type in one course
783      * @return array of pix_icon
784      */
785     public function get_info_icons(array $instances) {
786         return array();
787     }
789     /**
790      * Returns optional enrolment instance description text.
791      *
792      * This is used in detailed course information.
793      *
794      *
795      * @param object $instance
796      * @return string short html text
797      */
798     public function get_description_text($instance) {
799         return null;
800     }
802     /**
803      * Makes sure config is loaded and cached.
804      * @return void
805      */
806     protected function load_config() {
807         if (!isset($this->config)) {
808             $name = $this->get_name();
809             if (!$config = get_config("enrol_$name")) {
810                 $config = new object();
811             }
812             $this->config = $config;
813         }
814     }
816     /**
817      * Returns plugin config value
818      * @param  string $name
819      * @param  string $default value if config does not exist yet
820      * @return string value or default
821      */
822     public function get_config($name, $default = NULL) {
823         $this->load_config();
824         return isset($this->config->$name) ? $this->config->$name : $default;
825     }
827     /**
828      * Sets plugin config value
829      * @param  string $name name of config
830      * @param  string $value string config value, null means delete
831      * @return string value
832      */
833     public function set_config($name, $value) {
834         $pluginname = $this->get_name();
835         $this->load_config();
836         if ($value === NULL) {
837             unset($this->config->$name);
838         } else {
839             $this->config->$name = $value;
840         }
841         set_config($name, $value, "enrol_$pluginname");
842     }
844     /**
845      * Does this plugin assign protected roles are can they be manually removed?
846      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
847      */
848     public function roles_protected() {
849         return true;
850     }
852     /**
853      * Does this plugin allow manual enrolments?
854      *
855      * @param stdClass $instance course enrol instance
856      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
857      *
858      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, trues means nobody may add more enrolments manually
859      */
860     public function allow_enrol(stdClass $instance) {
861         return false;
862     }
864     /**
865      * Does this plugin allow manual unenrolments?
866      *
867      * @param stdClass $instance course enrol instance
868      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
869      *
870      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
871      */
872     public function allow_unenrol(stdClass $instance) {
873         return false;
874     }
876     /**
877      * Does this plugin allow manual changes in user_enrolments table?
878      *
879      * All plugins allowing this must implement 'enrol/xxx:manage' capability
880      *
881      * @param stdClass $instance course enrol instance
882      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
883      */
884     public function allow_manage(stdClass $instance) {
885         return false;
886     }
888     /**
889      * Does this plugin support some way to user to self enrol?
890      *
891      * @param stdClass $instance course enrol instance
892      *
893      * @return bool - true means show "Enrol me in this course" link in course UI
894      */
895     public function show_enrolme_link(stdClass $instance) {
896         return false;
897     }
899     /**
900      * Attempt to automatically enrol current user in course without any interaction,
901      * calling code has to make sure the plugin and instance are active.
902      *
903      * @param stdClass $instance course enrol instance
904      * @param stdClass $user record
905      * @return bool|int false means not enrolled, integer means timeend
906      */
907     public function try_autoenrol(stdClass $instance) {
908         global $USER;
910         return false;
911     }
913     /**
914      * Attempt to automatically gain temporary guest access to course,
915      * calling code has to make sure the plugin and instance are active.
916      *
917      * @param stdClass $instance course enrol instance
918      * @param stdClass $user record
919      * @return bool|int false means no guest access, integer means timeend
920      */
921     public function try_guestaccess(stdClass $instance) {
922         global $USER;
924         return false;
925     }
927     /**
928      * Enrol user into course via enrol instance.
929      *
930      * @param stdClass $instance
931      * @param int $userid
932      * @param int $roleid optional role id
933      * @param int $timestart 0 means unknown
934      * @param int $timeend 0 means forever
935      * @return void
936      */
937     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
938         global $DB, $USER, $CFG; // CFG necessary!!!
940         if ($instance->courseid == SITEID) {
941             throw new coding_exception('invalid attempt to enrol into frontpage course!');
942         }
944         $name = $this->get_name();
945         $courseid = $instance->courseid;
947         if ($instance->enrol !== $name) {
948             throw new coding_exception('invalid enrol instance!');
949         }
950         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
952         $inserted = false;
953         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
954             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
955                 $ue->timestart    = $timestart;
956                 $ue->timeend      = $timeend;
957                 $ue->modifier     = $USER->id;
958                 $ue->timemodified = time();
959                 $DB->update_record('user_enrolments', $ue);
960             }
961         } else {
962             $ue = new object();
963             $ue->enrolid      = $instance->id;
964             $ue->status       = ENROL_USER_ACTIVE;
965             $ue->userid       = $userid;
966             $ue->timestart    = $timestart;
967             $ue->timeend      = $timeend;
968             $ue->modifier     = $USER->id;
969             $ue->timecreated  = time();
970             $ue->timemodified = $ue->timecreated;
971             $ue->id = $DB->insert_record('user_enrolments', $ue);
973             $inserted = true;
974         }
976         if ($roleid) {
977             if ($this->roles_protected()) {
978                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
979             } else {
980                 role_assign($roleid, $userid, $context->id);
981             }
982         }
984         if ($inserted) {
985             // add extra info and trigger event
986             $ue->courseid  = $courseid;
987             $ue->enrol     = $name;
988             events_trigger('user_enrolled', $ue);
989         }
991         // reset primitive require_login() caching
992         if ($userid == $USER->id) {
993             if (isset($USER->enrol['enrolled'][$courseid])) {
994                 unset($USER->enrol['enrolled'][$courseid]);
995             }
996             if (isset($USER->enrol['tempguest'][$courseid])) {
997                 unset($USER->enrol['tempguest'][$courseid]);
998                 $USER->access = remove_temp_roles($context, $USER->access);
999             }
1000         }
1001     }
1003     /**
1004      * Store user_enrolments changes and trigger event.
1005      *
1006      * @param object $ue
1007      * @param int $user id
1008      * @param int $status
1009      * @param int $timestart
1010      * @param int $timeend
1011      * @return void
1012      */
1013     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1014         global $DB, $USER;
1016         $name = $this->get_name();
1018         if ($instance->enrol !== $name) {
1019             throw new coding_exception('invalid enrol instance!');
1020         }
1022         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1023             // weird, user not enrolled
1024             return;
1025         }
1027         $modified = false;
1028         if (isset($status) and $ue->status != $status) {
1029             $ue->status = $status;
1030             $modified = true;
1031         }
1032         if (isset($timestart) and $ue->timestart != $timestart) {
1033             $ue->timestart = $timestart;
1034             $modified = true;
1035         }
1036         if (isset($timeend) and $ue->timeend != $timeend) {
1037             $ue->timeend = $timeend;
1038             $modified = true;
1039         }
1041         if (!$modified) {
1042             // no change
1043             return;
1044         }
1046         $ue->modifierid = $USER->id;
1047         $DB->update_record('user_enrolments', $ue);
1049         // trigger event
1050         $ue->courseid  = $instance->courseid;
1051         $ue->enrol     = $instance->name;
1052         events_trigger('user_unenrol_modified', $ue);
1053     }
1055     /**
1056      * Unenrol user from course,
1057      * the last unenrolment removes all remaining roles.
1058      *
1059      * @param stdClass $instance
1060      * @param int $userid
1061      * @return void
1062      */
1063     public function unenrol_user(stdClass $instance, $userid) {
1064         global $CFG, $USER, $DB;
1066         $name = $this->get_name();
1067         $courseid = $instance->courseid;
1069         if ($instance->enrol !== $name) {
1070             throw new coding_exception('invalid enrol instance!');
1071         }
1072         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1074         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1075             // weird, user not enrolled
1076             return;
1077         }
1079         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1080         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1082         // add extra info and trigger event
1083         $ue->courseid  = $courseid;
1084         $ue->enrol     = $name;
1086         $sql = "SELECT 'x'
1087                   FROM {user_enrolments} ue
1088                   JOIN {enrol} e ON (e.id = ue.enrolid)
1089                   WHERE ue.userid = :userid AND e.courseid = :courseid";
1090         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1091             $ue->lastenrol = false;
1092             events_trigger('user_unenrolled', $ue);
1093             // user still has some enrolments, no big cleanup yet
1094         } else {
1095             // the big cleanup IS necessary!
1097             require_once("$CFG->dirroot/group/lib.php");
1098             require_once("$CFG->libdir/gradelib.php");
1100             // remove all remaining roles
1101             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1103             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1104             groups_delete_group_members($courseid, $userid);
1106             grade_user_unenrol($courseid, $userid);
1108             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1110             $ue->lastenrol = true; // means user not enrolled any more
1111             events_trigger('user_unenrolled', $ue);
1112         }
1113         // reset primitive require_login() caching
1114         if ($userid == $USER->id) {
1115             if (isset($USER->enrol['enrolled'][$courseid])) {
1116                 unset($USER->enrol['enrolled'][$courseid]);
1117             }
1118             if (isset($USER->enrol['tempguest'][$courseid])) {
1119                 unset($USER->enrol['tempguest'][$courseid]);
1120                 $USER->access = remove_temp_roles($context, $USER->access);
1121             }
1122         }
1123     }
1125     /**
1126      * Forces synchronisation of user enrolments.
1127      *
1128      * This is important especially for external enrol plugins,
1129      * this function is called for all enabled enrol plugins
1130      * right after every user login.
1131      *
1132      * @param object $user user record
1133      * @return void
1134      */
1135     public function sync_user_enrolments($user) {
1136         // override if necessary
1137     }
1139     /**
1140      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1141      * @param int $courseid
1142      * @return moodle_url page url
1143      */
1144     public function get_newinstance_link($courseid) {
1145         // override for most plugins, check if instance already exists in cases only one instance is supported
1146         return NULL;
1147     }
1149     /**
1150      * Is it possible to delete enrol instance via standard UI?
1151      *
1152      * @param object $instance
1153      * @return bool
1154      */
1155     public function instance_deleteable($instance) {
1156         return true;
1157     }
1159     /**
1160      * Returns link to manual enrol UI if exists.
1161      * Does the access control tests automatically.
1162      *
1163      * @param object $instance
1164      * @return moodle_url
1165      */
1166     public function get_manual_enrol_link($instance) {
1167         return NULL;
1168     }
1170     /**
1171      * Returns list of unenrol links for all enrol instances in course.
1172      *
1173      * @param int $instance
1174      * @return moodle_url or NULL if self unenrolment not supported
1175      */
1176     public function get_unenrolself_link($instance) {
1177         global $USER, $CFG, $DB;
1179         $name = $this->get_name();
1180         if ($instance->enrol !== $name) {
1181             throw new coding_exception('invalid enrol instance!');
1182         }
1184         if ($instance->courseid == SITEID) {
1185             return NULL;
1186         }
1188         if (!enrol_is_enabled($name)) {
1189             return NULL;
1190         }
1192         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1193             return NULL;
1194         }
1196         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1197             return NULL;
1198         }
1200         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1202         if (!has_capability("enrol/$name:unenrolself", $context)) {
1203             return NULL;
1204         }
1206         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1207             return NULL;
1208         }
1210         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1211     }
1213     /**
1214      * Adds enrol instance UI to course edit form
1215      *
1216      * @param object $instance enrol instance or null if does not exist yet
1217      * @param MoodleQuickForm $mform
1218      * @param object $data
1219      * @param object $context context of existing course or parent category if course does not exist
1220      * @return void
1221      */
1222     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1223         // override - usually at least enable/disable switch, has to add own form header
1224     }
1226     /**
1227      * Validates course edit form data
1228      *
1229      * @param object $instance enrol instance or null if does not exist yet
1230      * @param array $data
1231      * @param object $context context of existing course or parent category if course does not exist
1232      * @return array errors array
1233      */
1234     public function course_edit_validation($instance, array $data, $context) {
1235         return array();
1236     }
1238     /**
1239      * Called after updating/inserting course.
1240      *
1241      * @param bool $inserted true if course just inserted
1242      * @param object $course
1243      * @param object $data form data
1244      * @return void
1245      */
1246     public function course_updated($inserted, $course, $data) {
1247         if ($inserted) {
1248             if ($this->get_config('defaultenrol')) {
1249                 $this->add_default_instance($course);
1250             }
1251         }
1252     }
1254     /**
1255      * Add new instance of enrol plugin.
1256      * @param object $course
1257      * @param array instance fields
1258      * @return int id of new instance, null if can not be created
1259      */
1260     public function add_instance($course, array $fields = NULL) {
1261         global $DB;
1263         if ($course->id == SITEID) {
1264             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1265         }
1267         $instance = new object();
1268         $instance->enrol          = $this->get_name();
1269         $instance->status         = ENROL_INSTANCE_ENABLED;
1270         $instance->courseid       = $course->id;
1271         $instance->enrolstartdate = 0;
1272         $instance->enrolenddate   = 0;
1273         $instance->timemodified   = time();
1274         $instance->timecreated    = $instance->timemodified;
1275         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1277         $fields = (array)$fields;
1278         unset($fields['enrol']);
1279         unset($fields['courseid']);
1280         unset($fields['sortorder']);
1281         foreach($fields as $field=>$value) {
1282             $instance->$field = $value;
1283         }
1285         return $DB->insert_record('enrol', $instance);
1286     }
1288     /**
1289      * Add new instance of enrol plugin with default settings,
1290      * called when adding new instance manually or when adding new course.
1291      *
1292      * Not all plugins support this.
1293      *
1294      * @param object $course
1295      * @return int id of new instance or null if no default supported
1296      */
1297     public function add_default_instance($course) {
1298         return null;
1299     }
1301     /**
1302      * Delete course enrol plugin instance, unenrol all users.
1303      * @param object $instance
1304      * @return void
1305      */
1306     public function delete_instance($instance) {
1307         global $DB;
1309         $name = $this->get_name();
1310         if ($instance->enrol !== $name) {
1311             throw new coding_exception('invalid enrol instance!');
1312         }
1314         //first unenrol all users
1315         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1316         foreach ($participants as $participant) {
1317             $this->unenrol_user($instance, $participant->userid);
1318         }
1319         $participants->close();
1321         // now clean up all remainders that were not removed correctly
1322         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1323         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1325         // finally drop the enrol row
1326         $DB->delete_records('enrol', array('id'=>$instance->id));
1327     }
1329     /**
1330      * Creates course enrol form, checks if form submitted
1331      * and enrols user if necessary. It can also redirect.
1332      *
1333      * @param stdClass $instance
1334      * @return string html text, usually a form in a text box
1335      */
1336     public function enrol_page_hook(stdClass $instance) {
1337         return null;
1338     }
1340     /**
1341      * Adds navigation links into course admin block.
1342      *
1343      * By defaults looks for manage links only.
1344      *
1345      * @param navigation_node $instancesnode
1346      * @param object $instance
1347      * @return void
1348      */
1349     public function add_course_navigation($instancesnode, stdClass $instance) {
1350         // usually adds manage users
1351     }
1353     /**
1354      * Returns edit icons for the page with list of instances
1355      * @param stdClass $instance
1356      * @return array
1357      */
1358     public function get_action_icons(stdClass $instance) {
1359         return array();
1360     }
1362     /**
1363      * Reads version.php and determines if it is necessary
1364      * to execute the cron job now.
1365      * @return bool
1366      */
1367     public function is_cron_required() {
1368         global $CFG;
1370         $name = $this->get_name();
1371         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1372         $plugin = new object();
1373         include($versionfile);
1374         if (empty($plugin->cron)) {
1375             return false;
1376         }
1377         $lastexecuted = $this->get_config('lastcron', 0);
1378         if ($lastexecuted + $plugin->cron < time()) {
1379             return true;
1380         } else {
1381             return false;
1382         }
1383     }
1385     /**
1386      * Called for all enabled enrol plugins that returned true from is_cron_required().
1387      * @return void
1388      */
1389     public function cron() {
1390     }
1392     /**
1393      * Called when user is about to be deleted
1394      * @param object $user
1395      * @return void
1396      */
1397     public function user_delete($user) {
1398         global $DB;
1400         $sql = "SELECT e.*
1401                   FROM {enrol} e
1402                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1403                  WHERE e.enrol = :meta AND ue.userid = :userid";
1404         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1406         $rs = $DB->get_records_recordset($sql, $params);
1407         foreach($rs as $instance) {
1408             $this->unenrol_user($instance, $user->id);
1409         }
1410         $rs->close();
1411     }