MDL-21782 fixed wrong removal of hidden courses - thanks Jay Knight for the report!
[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) {
294     $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
296     $instances = enrol_get_instances($course->id, true);
297     $plugins   = enrol_get_plugins(true);
299     // we do not want to break all course pages if there is some borked enrol plugin, right?
300     foreach ($instances as $k=>$instance) {
301         if (!isset($plugins[$instance->enrol])) {
302             unset($instances[$k]);
303         }
304     }
306     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
308     if ($course->id != SITEID) {
309         // list all participants - allows assigning roles, groups, etc.
310         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
311             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
312             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/users', ''));
313         }
315         // manage enrol plugin instances
316         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
317             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
318         } else {
319             $url = NULL;
320         }
321         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
323         // each instance decides how to configure itself or how many other nav items are exposed
324         foreach ($instances as $instance) {
325             if (!isset($plugins[$instance->enrol])) {
326                 continue;
327             }
328             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
329         }
331         if (!$url) {
332             $instancesnode->trim_if_empty();
333         }
334     }
336     // Manage groups in this course or even frontpage
337     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
338         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
339         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
340     }
342      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
343         // Override roles
344         if (has_capability('moodle/role:review', $coursecontext)) {
345             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
346         } else {
347             $url = NULL;
348         }
349         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
351         // Add assign or override roles if allowed
352         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
353             if (has_capability('moodle/role:assign', $coursecontext)) {
354                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
355                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/roles', ''));
356             }
357         }
358         // Check role permissions
359         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
360             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
361             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
362         }
363      }
365      // Deal somehow with users that are not enrolled but still got a role somehow
366     if ($course->id != SITEID) {
367         //TODO, create some new UI for role assignments at course level
368         if (has_capability('moodle/role:assign', $coursecontext)) {
369             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
370             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/roles', ''));
371         }
372     }
374     // just in case nothing was actually added
375     $usersnode->trim_if_empty();
377     if ($course->id != SITEID) {
378         // Unenrol link
379         if (is_enrolled($coursecontext)) {
380             foreach ($instances as $instance) {
381                 if (!isset($plugins[$instance->enrol])) {
382                     continue;
383                 }
384                 $plugin = $plugins[$instance->enrol];
385                 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
386                     $coursenode->add(get_string('unenrolme', 'core_enrol', format_string($course->shortname)), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
387                     break;
388                     //TODO. deal with multiple unenrol links - not likely case, but still...
389                 }
390             }
391         } else {
392             if (is_viewing($coursecontext)) {
393                 // better not show any enrol link, this is intended for managers and inspectors
394             } else {
395                 foreach ($instances as $instance) {
396                     if (!isset($plugins[$instance->enrol])) {
397                         continue;
398                     }
399                     $plugin = $plugins[$instance->enrol];
400                     if ($plugin->show_enrolme_link($instance)) {
401                         $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
402                         $coursenode->add(get_string('enrolme', 'core_enrol', format_string($course->shortname)), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
403                         break;
404                     }
405                 }
406             }
407         }
408     }
411 /**
412  * Returns list of courses current $USER is enrolled in and can access
413  *
414  * - $fields is an array of field names to ADD
415  *   so name the fields you really need, which will
416  *   be added and uniq'd
417  *
418  * @param strin|array $fields
419  * @param string $sort
420  * @param int $limit max number of courses
421  * @return array
422  */
423 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
424     global $DB, $USER;
426     // Guest account does not have any courses
427     if (isguestuser() or !isloggedin()) {
428         return(array());
429     }
431     $basefields = array('id', 'category', 'sortorder',
432                         'shortname', 'fullname', 'idnumber',
433                         'startdate', 'visible',
434                         'groupmode', 'groupmodeforce');
436     if (empty($fields)) {
437         $fields = $basefields;
438     } else if (is_string($fields)) {
439         // turn the fields from a string to an array
440         $fields = explode(',', $fields);
441         $fields = array_map('trim', $fields);
442         $fields = array_unique(array_merge($basefields, $fields));
443     } else if (is_array($fields)) {
444         $fields = array_unique(array_merge($basefields, $fields));
445     } else {
446         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
447     }
448     if (in_array('*', $fields)) {
449         $fields = array('*');
450     }
452     $orderby = "";
453     $sort    = trim($sort);
454     if (!empty($sort)) {
455         $rawsorts = explode(',', $sort);
456         $sorts = array();
457         foreach ($rawsorts as $rawsort) {
458             $rawsort = trim($rawsort);
459             if (strpos($rawsort, 'c.') === 0) {
460                 $rawsort = substr($rawsort, 2);
461             }
462             $sorts[] = trim($rawsort);
463         }
464         $sort = 'c.'.implode(',c.', $sorts);
465         $orderby = "ORDER BY $sort";
466     }
468     $wheres = array("c.id <> :siteid");
469     $params = array('siteid'=>SITEID);
471     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
472         // list _only_ this course - anything else is asking for trouble...
473         $wheres[] = "courseid = :loginas";
474         $params['loginas'] = $USER->loginascontext->instanceid;
475     }
477     $coursefields = 'c.' .join(',c.', $fields);
478     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
479     $wheres = implode(" AND ", $wheres);
481     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
482     $sql = "SELECT $coursefields $ccselect
483               FROM {course} c
484               JOIN (SELECT DISTINCT e.courseid
485                       FROM {enrol} e
486                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
487                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
488                    ) en ON (en.courseid = c.id)
489            $ccjoin
490              WHERE $wheres
491           $orderby";
492     $params['userid']  = $USER->id;
493     $params['active']  = ENROL_USER_ACTIVE;
494     $params['enabled'] = ENROL_INSTANCE_ENABLED;
495     $params['now1']    = round(time(), -2); // improves db caching
496     $params['now2']    = $params['now1'];
498     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
500     // preload contexts and check visibility
501     foreach ($courses as $id=>$course) {
502         context_instance_preload($course);
503         if (!$course->visible) {
504             if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
505                 unset($courses[$id]);
506                 continue;
507             }
508             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
509                 unset($courses[$id]);
510                 continue;
511             }
512         }
513         $courses[$id] = $course;
514     }
516     //wow! Is that really all? :-D
518     return $courses;
521 /**
522  * Returns list of courses user is enrolled into.
523  *
524  * - $fields is an array of fieldnames to ADD
525  *   so name the fields you really need, which will
526  *   be added and uniq'd
527  *
528  * @param int $userid
529  * @param bool $onlyactive return only active enrolments in courses user may see
530  * @param strin|array $fields
531  * @param string $sort
532  * @return array
533  */
534 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
535     global $DB;
537     // Guest account does not have any courses
538     if (isguestuser($userid) or empty($userid)) {
539         return(array());
540     }
542     $basefields = array('id', 'category', 'sortorder',
543                         'shortname', 'fullname', 'idnumber',
544                         'startdate', 'visible',
545                         'groupmode', 'groupmodeforce');
547     if (empty($fields)) {
548         $fields = $basefields;
549     } else if (is_string($fields)) {
550         // turn the fields from a string to an array
551         $fields = explode(',', $fields);
552         $fields = array_map('trim', $fields);
553         $fields = array_unique(array_merge($basefields, $fields));
554     } else if (is_array($fields)) {
555         $fields = array_unique(array_merge($basefields, $fields));
556     } else {
557         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
558     }
559     if (in_array('*', $fields)) {
560         $fields = array('*');
561     }
563     $orderby = "";
564     $sort    = trim($sort);
565     if (!empty($sort)) {
566         $rawsorts = explode(',', $sort);
567         $sorts = array();
568         foreach ($rawsorts as $rawsort) {
569             $rawsort = trim($rawsort);
570             if (strpos($rawsort, 'c.') === 0) {
571                 $rawsort = substr($rawsort, 2);
572             }
573             $sorts[] = trim($rawsort);
574         }
575         $sort = 'c.'.implode(',c.', $sorts);
576         $orderby = "ORDER BY $sort";
577     }
579     $params = array('siteid'=>SITEID);
581     if ($onlyactive) {
582         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
583         $params['now1']    = round(time(), -2); // improves db caching
584         $params['now2']    = $params['now1'];
585         $params['active']  = ENROL_USER_ACTIVE;
586         $params['enabled'] = ENROL_INSTANCE_ENABLED;
587     } else {
588         $subwhere = "";
589     }
591     $coursefields = 'c.' .join(',c.', $fields);
592     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
594     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
595     $sql = "SELECT $coursefields $ccselect
596               FROM {course} c
597               JOIN (SELECT DISTINCT e.courseid
598                       FROM {enrol} e
599                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
600                  $subwhere
601                    ) en ON (en.courseid = c.id)
602            $ccjoin
603              WHERE c.id <> :siteid
604           $orderby";
605     $params['userid']  = $userid;
607     $courses = $DB->get_records_sql($sql, $params);
609     // preload contexts and check visibility
610     foreach ($courses as $id=>$course) {
611         context_instance_preload($course);
612         if ($onlyactive) {
613             if (!$course->visible) {
614                 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
615                     unset($course[$id]);
616                     continue;
617                 }
618                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
619                     unset($course[$id]);
620                     continue;
621                 }
622             }
623         }
624         $courses[$id] = $course;
625     }
627     //wow! Is that really all? :-D
629     return $courses;
633 /**
634  * Called when user is about to be deleted.
635  * @param object $user
636  * @return void
637  */
638 function enrol_user_delete($user) {
639     global $DB;
641     $plugins = enrol_get_plugins(true);
642     foreach ($plugins as $plugin) {
643         $plugin->user_delete($user);
644     }
646     // force cleanup of all broken enrolments
647     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
650 /**
651  * Try to enrol user via default internal auth plugin.
652  *
653  * For now this is always using the manual enrol plugin...
654  *
655  * @param $courseid
656  * @param $userid
657  * @param $roleid
658  * @param $timestart
659  * @param $timeend
660  * @return bool success
661  */
662 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
663     global $DB;
665     //note: this is hardcoded to manual plugin for now
667     if (!enrol_is_enabled('manual')) {
668         return false;
669     }
671     if (!$enrol = enrol_get_plugin('manual')) {
672         return false;
673     }
674     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
675         return false;
676     }
677     $instance = reset($instances);
679     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
681     return true;
684 /**
685  * All enrol plugins should be based on this class,
686  * this is also the main source of documentation.
687  */
688 abstract class enrol_plugin {
689     protected $config = null;
691     /**
692      * Returns name of this enrol plugin
693      * @return string
694      */
695     public function get_name() {
696         // second word in class is always enrol name
697         $words = explode('_', get_class($this));
698         return $words[1];
699     }
701     /**
702      * Returns localised name of enrol instance
703      *
704      * @param object $instance (null is accepted too)
705      * @return string
706      */
707     public function get_instance_name($instance) {
708         if (empty($instance->name)) {
709             $enrol = $this->get_name();
710             return get_string('pluginname', 'enrol_'.$enrol);
711         } else {
712             return format_string($instance->name);
713         }
714     }
716     /**
717      * Makes sure config is loaded and cached.
718      * @return void
719      */
720     protected function load_config() {
721         if (!isset($this->config)) {
722             $name = $this->get_name();
723             if (!$config = get_config("enrol_$name")) {
724                 $config = new object();
725             }
726             $this->config = $config;
727         }
728     }
730     /**
731      * Returns plugin config value
732      * @param  string $name
733      * @param  string $default value if config does not exist yet
734      * @return string value or default
735      */
736     public function get_config($name, $default = NULL) {
737         $this->load_config();
738         return isset($this->config->$name) ? $this->config->$name : $default;
739     }
741     /**
742      * Sets plugin config value
743      * @param  string $name name of config
744      * @param  string $value string config value, null means delete
745      * @return string value
746      */
747     public function set_config($name, $value) {
748         $pluginname = $this->get_name();
749         $this->load_config();
750         if ($value === NULL) {
751             unset($this->config->$name);
752         } else {
753             $this->config->$name = $value;
754         }
755         set_config($name, $value, "enrol_$pluginname");
756     }
758     /**
759      * Does this plugin assign protected roles are can they be manually removed?
760      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
761      */
762     public function roles_protected() {
763         return true;
764     }
766     /**
767      * Does this plugin allow manual enrolments?
768      *
769      * @param stdClass $instance course enrol instance
770      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
771      *
772      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, trues means nobody may add more enrolments manually
773      */
774     public function allow_enrol(stdClass $instance) {
775         return false;
776     }
778     /**
779      * Does this plugin allow manual unenrolments?
780      *
781      * @param stdClass $instance course enrol instance
782      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
783      *
784      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
785      */
786     public function allow_unenrol(stdClass $instance) {
787         return false;
788     }
790     /**
791      * Does this plugin allow manual changes in user_enrolments table?
792      *
793      * All plugins allowing this must implement 'enrol/xxx:manage' capability
794      *
795      * @param stdClass $instance course enrol instance
796      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
797      */
798     public function allow_manage(stdClass $instance) {
799         return false;
800     }
802     /**
803      * Does this plugin support some way to user to self enrol?
804      *
805      * @param stdClass $instance course enrol instance
806      *
807      * @return bool - true means show "Enrol me in this course" link in course UI
808      */
809     public function show_enrolme_link(stdClass $instance) {
810         return false;
811     }
813     /**
814      * Attempt to automatically enrol current user in course without any interaction,
815      * calling code has to make sure the plugin and instance are active.
816      *
817      * @param stdClass $instance course enrol instance
818      * @param stdClass $user record
819      * @return bool|int false means not enrolled, integer means timeend
820      */
821     public function try_autoenrol(stdClass $instance) {
822         global $USER;
824         return false;
825     }
827     /**
828      * Attempt to automatically gain temporary guest access to course,
829      * calling code has to make sure the plugin and instance are active.
830      *
831      * @param stdClass $instance course enrol instance
832      * @param stdClass $user record
833      * @return bool|int false means no guest access, integer means timeend
834      */
835     public function try_guestaccess(stdClass $instance) {
836         global $USER;
838         return false;
839     }
841     /**
842      * Enrol user into course via enrol instance.
843      *
844      * @param stdClass $instance
845      * @param int $userid
846      * @param int $roleid optional role id
847      * @param int $timestart 0 means unknown
848      * @param int $timeend 0 means forever
849      * @return void
850      */
851     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
852         global $DB, $USER, $CFG; // CFG necessary!!!
854         if ($instance->courseid == SITEID) {
855             throw new coding_exception('invalid attempt to enrol into frontpage course!');
856         }
858         $name = $this->get_name();
859         $courseid = $instance->courseid;
861         if ($instance->enrol !== $name) {
862             throw new coding_exception('invalid enrol instance!');
863         }
864         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
866         $inserted = false;
867         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
868             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
869                 $ue->timestart    = $timestart;
870                 $ue->timeend      = $timeend;
871                 $ue->modifier     = $USER->id;
872                 $ue->timemodified = time();
873                 $DB->update_record('user_enrolments', $ue);
874             }
875         } else {
876             $ue = new object();
877             $ue->enrolid      = $instance->id;
878             $ue->status       = ENROL_USER_ACTIVE;
879             $ue->userid       = $userid;
880             $ue->timestart    = $timestart;
881             $ue->timeend      = $timeend;
882             $ue->modifier     = $USER->id;
883             $ue->timecreated  = time();
884             $ue->timemodified = $ue->timecreated;
885             $ue->id = $DB->insert_record('user_enrolments', $ue);
887             $inserted = true;
888         }
890         if ($roleid) {
891             if ($this->roles_protected()) {
892                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
893             } else {
894                 role_assign($roleid, $userid, $context->id);
895             }
896         }
898         if ($inserted) {
899             // add extra info and trigger event
900             $ue->courseid  = $courseid;
901             $ue->enrol     = $name;
902             events_trigger('user_enrolled', $ue);
903         }
905         // reset primitive require_login() caching
906         if ($userid == $USER->id) {
907             if (isset($USER->enrol['enrolled'][$courseid])) {
908                 unset($USER->enrol['enrolled'][$courseid]);
909             }
910             if (isset($USER->enrol['tempguest'][$courseid])) {
911                 unset($USER->enrol['tempguest'][$courseid]);
912                 $USER->access = remove_temp_roles($context, $USER->access);
913             }
914         }
915     }
917     /**
918      * Store user_enrolments changes and trigger event.
919      *
920      * @param object $ue
921      * @param int $user id
922      * @param int $status
923      * @param int $timestart
924      * @param int $timeend
925      * @return void
926      */
927     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
928         global $DB, $USER;
930         $name = $this->get_name();
932         if ($instance->enrol !== $name) {
933             throw new coding_exception('invalid enrol instance!');
934         }
936         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
937             // weird, user not enrolled
938             return;
939         }
941         $modified = false;
942         if (isset($status) and $ue->status != $status) {
943             $ue->status = $status;
944             $modified = true;
945         }
946         if (isset($timestart) and $ue->timestart != $timestart) {
947             $ue->timestart = $timestart;
948             $modified = true;
949         }
950         if (isset($timeend) and $ue->timeend != $timeend) {
951             $ue->timeend = $timeend;
952             $modified = true;
953         }
955         if (!$modified) {
956             // no change
957             return;
958         }
960         $ue->modifierid = $USER->id;
961         $DB->update_record('user_enrolments', $ue);
963         // trigger event
964         $ue->courseid  = $instance->courseid;
965         $ue->enrol     = $instance->name;
966         events_trigger('user_unenrol_modified', $ue);
967     }
969     /**
970      * Unenrol user from course,
971      * the last unenrolment removes all remaining roles.
972      *
973      * @param stdClass $instance
974      * @param int $userid
975      * @return void
976      */
977     public function unenrol_user(stdClass $instance, $userid) {
978         global $CFG, $USER, $DB;
980         $name = $this->get_name();
981         $courseid = $instance->courseid;
983         if ($instance->enrol !== $name) {
984             throw new coding_exception('invalid enrol instance!');
985         }
986         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
988         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
989             // weird, user not enrolled
990             return;
991         }
993         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
994         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
996         // add extra info and trigger event
997         $ue->courseid  = $courseid;
998         $ue->enrol     = $name;
1000         $sql = "SELECT 'x'
1001                   FROM {user_enrolments} ue
1002                   JOIN {enrol} e ON (e.id = ue.enrolid)
1003                   WHERE ue.userid = :userid AND e.courseid = :courseid";
1004         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1005             $ue->lastenrol = false;
1006             events_trigger('user_unenrolled', $ue);
1007             // user still has some enrolments, no big cleanup yet
1008         } else {
1009             // the big cleanup IS necessary!
1011             require_once("$CFG->dirroot/group/lib.php");
1012             require_once("$CFG->libdir/gradelib.php");
1014             // remove all remaining roles
1015             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1017             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1018             groups_delete_group_members($courseid, $userid);
1020             grade_user_unenrol($courseid, $userid);
1022             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1024             $ue->lastenrol = true; // means user not enrolled any more
1025             events_trigger('user_unenrolled', $ue);
1026         }
1027         // reset primitive require_login() caching
1028         if ($userid == $USER->id) {
1029             if (isset($USER->enrol['enrolled'][$courseid])) {
1030                 unset($USER->enrol['enrolled'][$courseid]);
1031             }
1032             if (isset($USER->enrol['tempguest'][$courseid])) {
1033                 unset($USER->enrol['tempguest'][$courseid]);
1034                 $USER->access = remove_temp_roles($context, $USER->access);
1035             }
1036         }
1037     }
1039     /**
1040      * Forces synchronisation of user enrolments.
1041      *
1042      * This is important especially for external enrol plugins,
1043      * this function is called for all enabled enrol plugins
1044      * right after every user login.
1045      *
1046      * @param object $user user record
1047      * @return void
1048      */
1049     public function sync_user_enrolments($user) {
1050         // override if necessary
1051     }
1053     /**
1054      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1055      * @param int $courseid
1056      * @return moodle_url page url
1057      */
1058     public function get_candidate_link($courseid) {
1059         // override for most plugins, check if instance already exists in cases only one instance is supported
1060         return NULL;
1061     }
1063     /**
1064      * Is it possible to delete enrol instance via standard UI?
1065      *
1066      * @param object $instance
1067      * @return bool
1068      */
1069     public function instance_deleteable($instance) {
1070         return true;
1071     }
1073     /**
1074      * Returns link to manual enrol UI if exists.
1075      * Does the access control tests automatically.
1076      *
1077      * @param object $instance
1078      * @return moodle_url
1079      */
1080     public function get_manual_enrol_link($instance) {
1081         return NULL;
1082     }
1084     /**
1085      * Returns list of unenrol links for all enrol instances in course.
1086      *
1087      * @param int $instance
1088      * @return moodle_url or NULL if self unernolmnet not supported
1089      */
1090     public function get_unenrolself_link($instance) {
1091         global $USER, $CFG, $DB;
1093         $name = $this->get_name();
1094         if ($instance->enrol !== $name) {
1095             throw new coding_exception('invalid enrol instance!');
1096         }
1098         if ($instance->courseid == SITEID) {
1099             return NULL;
1100         }
1102         if (!enrol_is_enabled($name)) {
1103             return NULL;
1104         }
1106         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1107             return NULL;
1108         }
1110         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1111             return NULL;
1112         }
1114         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1116         if (!has_capability("enrol/$name:unenrolself", $context)) {
1117             return NULL;
1118         }
1120         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1121             return NULL;
1122         }
1124         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1125     }
1127     /**
1128      * Adds enrol instance UI to course edit form
1129      *
1130      * @param object $instance enrol instance or null if does not exist yet
1131      * @param MoodleQuickForm $mform
1132      * @param object $data
1133      * @param object $context context of existing course or parent category if course does not exist
1134      * @return void
1135      */
1136     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1137         // override - usually at least enable/disable switch, has to add own form header
1138     }
1140     /**
1141      * Validates course edit form data
1142      *
1143      * @param object $instance enrol instance or null if does not exist yet
1144      * @param array $data
1145      * @param object $context context of existing course or parent category if course does not exist
1146      * @return array errors array
1147      */
1148     public function course_edit_validation($instance, array $data, $context) {
1149         return array();
1150     }
1152     /**
1153      * Called after updating/inserting course.
1154      *
1155      * @param bool $inserted true if course just inserted
1156      * @param object $course
1157      * @param object $data form data
1158      * @return void
1159      */
1160     public function course_updated($inserted, $course, $data) {
1161         // override if settings on course edit page or some automatic sync needed
1162     }
1164     /**
1165      * Add new instance of enrol plugin settings.
1166      * @param object $course
1167      * @param array instance fields
1168      * @return int id of new instance
1169      */
1170     public function add_instance($course, array $fields = NULL) {
1171         global $DB;
1173         if ($course->id == SITEID) {
1174             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1175         }
1177         $instance = new object();
1178         $instance->enrol          = $this->get_name();
1179         $instance->status         = ENROL_INSTANCE_ENABLED;
1180         $instance->courseid       = $course->id;
1181         $instance->enrolstartdate = 0;
1182         $instance->enrolenddate   = 0;
1183         $instance->timemodified   = time();
1184         $instance->timecreated    = $instance->timemodified;
1185         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1187         $fields = (array)$fields;
1188         unset($fields['enrol']);
1189         unset($fields['courseid']);
1190         unset($fields['sortorder']);
1191         foreach($fields as $field=>$value) {
1192             $instance->$field = $value;
1193         }
1195         return $DB->insert_record('enrol', $instance);
1196     }
1198     /**
1199      * Add new instance of enrol plugin with default settings,
1200      * called when adding new instance manually or when adding new course.
1201      *
1202      * Not all plugins support this.
1203      *
1204      * @param object $course
1205      * @return int id of new instance or null if no default supported
1206      */
1207     public function add_default_instance($course) {
1208         return null;
1209     }
1211     /**
1212      * Delete course enrol plugin instance, unenrol all users.
1213      * @param object $instance
1214      * @return void
1215      */
1216     public function delete_instance($instance) {
1217         global $DB;
1219         $name = $this->get_name();
1220         if ($instance->enrol !== $name) {
1221             throw new coding_exception('invalid enrol instance!');
1222         }
1224         //first unenrol all users
1225         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1226         foreach ($participants as $participant) {
1227             $this->unenrol_user($instance, $participant->userid);
1228         }
1229         $participants->close();
1231         // now clean up all remainders that were not removed correctly
1232         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1233         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1235         // finally drop the enrol row
1236         $DB->delete_records('enrol', array('id'=>$instance->id));
1237     }
1239     /**
1240      * Creates course enrol form, checks if form submitted
1241      * and enrols user if necessary. It can also redirect.
1242      *
1243      * @param stdClass $instance
1244      * @return string html text, usually a form in a text box
1245      */
1246     public function enrol_page_hook(stdClass $instance) {
1247         return null;
1248     }
1250     /**
1251      * Adds navigation links into course admin block.
1252      *
1253      * By defaults looks for manage links only.
1254      *
1255      * @param navigation_node $instancesnode
1256      * @param object $instance
1257      * @return moodle_url;
1258      */
1259     public function add_course_navigation($instancesnode, stdClass $instance) {
1260         if ($managelink = $this->get_manage_link($instance)) {
1261             $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
1262         }
1263     }
1265     /**
1266      * Returns enrolment instance manage link.
1267      *
1268      * By defaults looks for manage.php file and tests for manage capability.
1269      *
1270      * @param object $instance
1271      * @return moodle_url;
1272      */
1273     public function get_manage_link($instance) {
1274         global $CFG, $DB;
1276         $name = $this->get_name();
1278         if ($instance->enrol !== $name) {
1279              throw new coding_exception('Invalid enrol instance type!');
1280         }
1282         if (!file_exists("$CFG->dirroot/enrol/$name/manage.php")) {
1283             return NULL;
1284         }
1286         if ($instance->courseid == SITEID) {
1287             // no enrolments on the frontpage, only roles there allowed
1288             return NULL;
1289         }
1291         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
1292         if (!has_capability('enrol/'.$name.':manage', $context)) {
1293             return NULL;
1294         }
1296         return new moodle_url("/enrol/$name/manage.php", array('enrolid'=>$instance->id));
1297     }
1299     /**
1300      * Reads version.php and determines if it is necessary
1301      * to execute the cron job now.
1302      * @return bool
1303      */
1304     public function is_cron_required() {
1305         global $CFG;
1307         $name = $this->get_name();
1308         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1309         $plugin = new object();
1310         include($versionfile);
1311         if (empty($plugin->cron)) {
1312             return false;
1313         }
1314         $lastexecuted = $this->get_config('lastcron', 0);
1315         if ($lastexecuted + $plugin->cron < time()) {
1316             return true;
1317         } else {
1318             return false;
1319         }
1320     }
1322     /**
1323      * Called for all enabled enrol plugins that returned true from is_cron_required().
1324      * @return void
1325      */
1326     public function cron() {
1327     }
1329     /**
1330      * Called when user is about to be deleted
1331      * @param object $user
1332      * @return void
1333      */
1334     public function user_delete($user) {
1335         global $DB;
1337         $sql = "SELECT e.*
1338                   FROM {enrol} e
1339                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1340                  WHERE e.enrol = :meta AND ue.userid = :userid";
1341         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1343         $rs = $DB->get_records_recordset($sql, $params);
1344         foreach($rs as $instance) {
1345             $this->unenrol_user($instance, $user->id);
1346         }
1347         $rs->close();
1348     }