b681ec444c66722fc9c3b6c7e6e9a4d174ebffcd
[moodle.git] / lib / enrollib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * This library includes the basic parts of enrol api.
20  * It is available on each page.
21  *
22  * @package   moodlecore
23  * @copyright 2010 Petr Skoda {@link http://skodak.org}
24  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
28 /** Course enrol instance enabled. (used in enrol->status) */
29 define('ENROL_INSTANCE_ENABLED', 0);
31 /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
32 define('ENROL_INSTANCE_DISABLED', 1);
34 /** User is active participant (used in user_enrolments->status)*/
35 define('ENROL_USER_ACTIVE', 0);
37 /** User participation in course is suspended (used in user_enrolments->status) */
38 define('ENROL_USER_SUSPENDED', 1);
40 /** Enrol info is cached for this number of seconds in require_login() */
41 define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
43 /** When user disappears from external source, the enrolment is completely removed */
44 define('ENROL_EXT_REMOVED_UNENROL', 0);
46 /** When user disappears from external source, the enrolment is kept as is - one way sync */
47 define('ENROL_EXT_REMOVED_KEEP', 1);
49 /**
50  * When user disappears from external source, user enrolment is suspended, roles are kept as is.
51  * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
52  * assignments, etc.
53  */
54 define('ENROL_EXT_REMOVED_SUSPEND', 2);
56 /**
57  * When user disappears from external source, the enrolment is suspended and roles assigned
58  * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
59  * */
60 define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
62 /**
63  * Returns instances of enrol plugins
64  * @param bool $enable return enabled only
65  * @return array of enrol plugins name=>instance
66  */
67 function enrol_get_plugins($enabled) {
68     global $CFG;
70     $result = array();
72     if ($enabled) {
73         // sorted by enabled plugin order
74         $enabled = explode(',', $CFG->enrol_plugins_enabled);
75         $plugins = array();
76         foreach ($enabled as $plugin) {
77             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
78         }
79     } else {
80         // sorted alphabetically
81         $plugins = get_plugin_list('enrol');
82         ksort($plugins);
83     }
85     foreach ($plugins as $plugin=>$location) {
86         if (!file_exists("$location/lib.php")) {
87             continue;
88         }
89         include_once("$location/lib.php");
90         $class = "enrol_{$plugin}_plugin";
91         if (!class_exists($class)) {
92             continue;
93         }
95         $result[$plugin] = new $class();
96     }
98     return $result;
99 }
101 /**
102  * Returns instance of enrol plugin
103  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
104  * @return enrol_plugin
105  */
106 function enrol_get_plugin($name) {
107     global $CFG;
109     if ($name !== clean_param($name, PARAM_SAFEDIR)) {
110         // ignore malformed plugin names completely
111         return null;
112     }
114     $location = "$CFG->dirroot/enrol/$name";
116     if (!file_exists("$location/lib.php")) {
117         return null;
118     }
119     include_once("$location/lib.php");
120     $class = "enrol_{$name}_plugin";
121     if (!class_exists($class)) {
122         return null;
123     }
125     return new $class();
128 /**
129  * Returns enrolment instances in given course.
130  * @param int $courseid
131  * @param bool $enabled
132  * @return array of enrol instances
133  */
134 function enrol_get_instances($courseid, $enabled) {
135     global $DB, $CFG;
137     if (!$enabled) {
138         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
139     }
141     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
143     $enabled = explode(',', $CFG->enrol_plugins_enabled);
144     foreach ($result as $key=>$instance) {
145         if (!in_array($instance->enrol, $enabled)) {
146             unset($result[$key]);
147             continue;
148         }
149         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
150             // broken plugin
151             unset($result[$key]);
152             continue;
153         }
154     }
156     return $result;
159 /**
160  * Checks if a given plugin is in the list of enabled enrolment plugins.
161  *
162  * @param string $enrol Enrolment plugin name
163  * @return boolean Whether the plugin is enabled
164  */
165 function enrol_is_enabled($enrol) {
166     global $CFG;
168     if (empty($CFG->enrol_plugins_enabled)) {
169         return false;
170     }
171     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
174 /**
175  * Check all the login enrolment information for the given user object
176  * by querying the enrolment plugins
177  *
178  * @param object $user
179  * @return void
180  */
181 function enrol_check_plugins($user) {
182     global $CFG;
184     if (empty($user->id) or isguestuser($user)) {
185         // shortcut - there is no enrolment work for guests and not-logged-in users
186         return;
187     }
189     if (is_siteadmin()) {
190         // no sync for admin user, please use admin accounts only for admin tasks like the unix root user!
191         // if plugin fails on sync admins need to be able to log in
192         return;
193     }
195     static $inprogress = array();  // To prevent this function being called more than once in an invocation
197     if (!empty($inprogress[$user->id])) {
198         return;
199     }
201     $inprogress[$user->id] = true;  // Set the flag
203     $enabled = enrol_get_plugins(true);
205     foreach($enabled as $enrol) {
206         $enrol->sync_user_enrolments($user);
207     }
209     unset($inprogress[$user->id]);  // Unset the flag
212 /**
213  * This function adds necessary enrol plugins UI into the course edit form.
214  *
215  * @param MoodleQuickForm $mform
216  * @param object $data course edit form data
217  * @param object $context context of existing course or parent category if course does not exist
218  * @return void
219  */
220 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
221     $plugins = enrol_get_plugins(true);
222     if (!empty($data->id)) {
223         $instances = enrol_get_instances($data->id, false);
224         foreach ($instances as $instance) {
225             if (!isset($plugins[$instance->enrol])) {
226                 continue;
227             }
228             $plugin = $plugins[$instance->enrol];
229             $plugin->course_edit_form($instance, $mform, $data, $context);
230         }
231     } else {
232         foreach ($plugins as $plugin) {
233             $plugin->course_edit_form(NULL, $mform, $data, $context);
234         }
235     }
238 /**
239  * Validate course edit form data
240  *
241  * @param array $data raw form data
242  * @param object $context context of existing course or parent category if course does not exist
243  * @return array errors array
244  */
245 function enrol_course_edit_validation(array $data, $context) {
246     $errors = array();
247     $plugins = enrol_get_plugins(true);
249     if (!empty($data['id'])) {
250         $instances = enrol_get_instances($data['id'], false);
251         foreach ($instances as $instance) {
252             if (!isset($plugins[$instance->enrol])) {
253                 continue;
254             }
255             $plugin = $plugins[$instance->enrol];
256             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
257         }
258     } else {
259         foreach ($plugins as $plugin) {
260             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
261         }
262     }
264     return $errors;
267 /**
268  * Update enrol instances after course edit form submission
269  * @param bool $inserted true means new course added, false course already existed
270  * @param object $course
271  * @param object $data form data
272  * @return void
273  */
274 function enrol_course_updated($inserted, $course, $data) {
275     global $DB, $CFG;
277     $plugins = enrol_get_plugins(true);
279     foreach ($plugins as $plugin) {
280         $plugin->course_updated($inserted, $course, $data);
281     }
284 /**
285  * Add navigation nodes
286  * @param navigation_node $coursenode
287  * @param object $course
288  * @return void
289  */
290 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
292     $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
294     $instances = enrol_get_instances($course->id, true);
295     $plugins   = enrol_get_plugins(true);
297     // we do not want to break all course pages if there is some borked enrol plugin, right?
298     foreach ($instances as $k=>$instance) {
299         if (!isset($plugins[$instance->enrol])) {
300             unset($instances[$k]);
301         }
302     }
304     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
306     if ($course->id != SITEID) {
307         // list all participants - allows assigning roles, groups, etc.
308         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
309             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
310             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/users', ''));
311         }
313         // manage enrol plugin instances
314         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
315             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
316         } else {
317             $url = NULL;
318         }
319         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
321         // each instance decides how to configure itself or how many other nav items are exposed
322         foreach ($instances as $instance) {
323             if (!isset($plugins[$instance->enrol])) {
324                 continue;
325             }
326             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
327         }
329         if (!$url) {
330             $instancesnode->trim_if_empty();
331         }
332     }
334     // Manage groups in this course or even frontpage
335     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
336         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
337         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
338     }
340      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
341         // Override roles
342         if (has_capability('moodle/role:review', $coursecontext)) {
343             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
344         } else {
345             $url = NULL;
346         }
347         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
349         // Add assign or override roles if allowed
350         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
351             if (has_capability('moodle/role:assign', $coursecontext)) {
352                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
353                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/roles', ''));
354             }
355         }
356         // Check role permissions
357         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
358             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
359             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
360         }
361      }
363      // Deal somehow with users that are not enrolled but still got a role somehow
364     if ($course->id != SITEID) {
365         //TODO, create some new UI for role assignments at course level
366         if (has_capability('moodle/role:assign', $coursecontext)) {
367             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
368             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/roles', ''));
369         }
370     }
372     // just in case nothing was actually added
373     $usersnode->trim_if_empty();
375     if ($course->id != SITEID) {
376         // Unenrol link
377         $unenrolprinted = false;
378         foreach ($instances as $instance) {
379             if (!isset($plugins[$instance->enrol])) {
380                 continue;
381             }
382             $plugin = $plugins[$instance->enrol];
383             if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
384                 $coursenode->add(get_string('unenrolme', 'core_enrol', format_string($course->shortname)), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
385                 $unenrolprinted = true;
386                 //TODO. deal with multiple unenrol links - not likely case, but still...
387             }
388         }
389         // Enrol link
390         if (!$unenrolprinted and !is_viewing($coursecontext) and !is_enrolled($coursecontext)) {
391             $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
392             $coursenode->add(get_string('enrolme', 'core_enrol', format_string($course->shortname)), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
393         }
394     }
397 /**
398  * Returns list of courses current $USER is enrolled in and can access
399  *
400  * - $fields is an array of field names to ADD
401  *   so name the fields you really need, which will
402  *   be added and uniq'd
403  *
404  * @param strin|array $fields
405  * @param string $sort
406  * @param int $limit max number of courses
407  * @return array
408  */
409 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
410     global $DB, $USER;
412     // Guest account does not have any courses
413     if (isguestuser() or !isloggedin()) {
414         return(array());
415     }
417     $basefields = array('id', 'category', 'sortorder',
418                         'shortname', 'fullname', 'idnumber',
419                         'startdate', 'visible',
420                         'groupmode', 'groupmodeforce');
422     if (empty($fields)) {
423         $fields = $basefields;
424     } else if (is_string($fields)) {
425         // turn the fields from a string to an array
426         $fields = explode(',', $fields);
427         $fields = array_map('trim', $fields);
428         $fields = array_unique(array_merge($basefields, $fields));
429     } else if (is_array($fields)) {
430         $fields = array_unique(array_merge($basefields, $fields));
431     } else {
432         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
433     }
434     if (in_array('*', $fields)) {
435         $fields = array('*');
436     }
438     $orderby = "";
439     $sort    = trim($sort);
440     if (!empty($sort)) {
441         $rawsorts = explode(',', $sort);
442         $sorts = array();
443         foreach ($rawsorts as $rawsort) {
444             $rawsort = trim($rawsort);
445             if (strpos($rawsort, 'c.') === 0) {
446                 $rawsort = substr($rawsort, 2);
447             }
448             $sorts[] = trim($rawsort);
449         }
450         $sort = 'c.'.implode(',c.', $sorts);
451         $orderby = "ORDER BY $sort";
452     }
454     $wheres = array("c.id <> :siteid");
455     $params = array('siteid'=>SITEID);
457     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
458         // list _only_ this course - anything else is asking for trouble...
459         $wheres[] = "courseid = :loginas";
460         $params['loginas'] = $USER->loginascontext->instanceid;
461     }
463     $coursefields = 'c.' .join(',c.', $fields);
464     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
465     $wheres = implode(" AND ", $wheres);
467     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
468     $sql = "SELECT $coursefields $ccselect
469               FROM {course} c
470               JOIN (SELECT DISTINCT e.courseid
471                       FROM {enrol} e
472                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
473                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
474                    ) en ON (en.courseid = c.id)
475            $ccjoin
476              WHERE $wheres
477           $orderby";
478     $params['userid']  = $USER->id;
479     $params['active']  = ENROL_USER_ACTIVE;
480     $params['enabled'] = ENROL_INSTANCE_ENABLED;
481     $params['now1']    = round(time(), -2); // improves db caching
482     $params['now2']    = $params['now1'];
484     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
486     // preload contexts and check visibility
487     foreach ($courses as $id=>$course) {
488         context_instance_preload($course);
489         if (!$course->visible) {
490             if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
491                 unset($course[$id]);
492                 continue;
493             }
494             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
495                 unset($course[$id]);
496                 continue;
497             }
498         }
499         $courses[$id] = $course;
500     }
502     //wow! Is that really all? :-D
504     return $courses;
507 /**
508  * Returns list of courses user is enrolled into.
509  *
510  * - $fields is an array of fieldnames to ADD
511  *   so name the fields you really need, which will
512  *   be added and uniq'd
513  *
514  * @param int $userid
515  * @param bool $onlyactive return only active enrolments in courses user may see
516  * @param strin|array $fields
517  * @param string $sort
518  * @return array
519  */
520 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
521     global $DB;
523     // Guest account does not have any courses
524     if (isguestuser($userid) or empty($userid)) {
525         return(array());
526     }
528     $basefields = array('id', 'category', 'sortorder',
529                         'shortname', 'fullname', 'idnumber',
530                         'startdate', 'visible',
531                         'groupmode', 'groupmodeforce');
533     if (empty($fields)) {
534         $fields = $basefields;
535     } else if (is_string($fields)) {
536         // turn the fields from a string to an array
537         $fields = explode(',', $fields);
538         $fields = array_map('trim', $fields);
539         $fields = array_unique(array_merge($basefields, $fields));
540     } else if (is_array($fields)) {
541         $fields = array_unique(array_merge($basefields, $fields));
542     } else {
543         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
544     }
545     if (in_array('*', $fields)) {
546         $fields = array('*');
547     }
549     $orderby = "";
550     $sort    = trim($sort);
551     if (!empty($sort)) {
552         $rawsorts = explode(',', $sort);
553         $sorts = array();
554         foreach ($rawsorts as $rawsort) {
555             $rawsort = trim($rawsort);
556             if (strpos($rawsort, 'c.') === 0) {
557                 $rawsort = substr($rawsort, 2);
558             }
559             $sorts[] = trim($rawsort);
560         }
561         $sort = 'c.'.implode(',c.', $sorts);
562         $orderby = "ORDER BY $sort";
563     }
565     $params = array('siteid'=>SITEID);
567     if ($onlyactive) {
568         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
569         $params['now1']    = round(time(), -2); // improves db caching
570         $params['now2']    = $params['now1'];
571         $params['active']  = ENROL_USER_ACTIVE;
572         $params['enabled'] = ENROL_INSTANCE_ENABLED;
573     } else {
574         $subwhere = "";
575     }
577     $coursefields = 'c.' .join(',c.', $fields);
578     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
580     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
581     $sql = "SELECT $coursefields $ccselect
582               FROM {course} c
583               JOIN (SELECT DISTINCT e.courseid
584                       FROM {enrol} e
585                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
586                  $subwhere
587                    ) en ON (en.courseid = c.id)
588            $ccjoin
589              WHERE c.id <> :siteid
590           $orderby";
591     $params['userid']  = $userid;
593     $courses = $DB->get_records_sql($sql, $params);
595     // preload contexts and check visibility
596     foreach ($courses as $id=>$course) {
597         context_instance_preload($course);
598         if ($onlyactive) {
599             if (!$course->visible) {
600                 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
601                     unset($course[$id]);
602                     continue;
603                 }
604                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
605                     unset($course[$id]);
606                     continue;
607                 }
608             }
609         }
610         $courses[$id] = $course;
611     }
613     //wow! Is that really all? :-D
615     return $courses;
619 /**
620  * Called when user is about to be deleted.
621  * @param object $user
622  * @return void
623  */
624 function enrol_user_delete($user) {
625     global $DB;
627     $plugins = enrol_get_plugins(true);
628     foreach ($plugins as $plugin) {
629         $plugin->user_delete($user);
630     }
632     // force cleanup of all broken enrolments
633     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
636 /**
637  * Try to enrol user via default internal auth plugin.
638  *
639  * For now this is always using the manual enrol plugin...
640  *
641  * @param $courseid
642  * @param $userid
643  * @param $roleid
644  * @param $timestart
645  * @param $timeend
646  * @return bool success
647  */
648 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
649     global $DB;
651     //note: this is hardcoded to manual plugin for now
653     if (!enrol_is_enabled('manual')) {
654         return false;
655     }
657     if (!$enrol = enrol_get_plugin('manual')) {
658         return false;
659     }
660     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
661         return false;
662     }
663     $instance = reset($instances);
665     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
667     return true;
670 /**
671  * All enrol plugins should be based on this class,
672  * this is also the main source of documentation.
673  */
674 abstract class enrol_plugin {
675     protected $config = null;
677     /**
678      * Returns name of this enrol plugin
679      * @return string
680      */
681     public function get_name() {
682         // second word in class is always enrol name
683         $words = explode('_', get_class($this));
684         return $words[1];
685     }
687     /**
688      * Returns localised name of enrol instance
689      *
690      * @param object $instance (null is accepted too)
691      * @return string
692      */
693     public function get_instance_name($instance) {
694         if (empty($instance->name)) {
695             $enrol = $this->get_name();
696             return get_string('pluginname', 'enrol_'.$enrol);
697         } else {
698             return format_string($instance->name);
699         }
700     }
702     /**
703      * Makes sure config is loaded and cached.
704      * @return void
705      */
706     protected function load_config() {
707         if (!isset($this->config)) {
708             $name = $this->get_name();
709             if (!$config = get_config("enrol_$name")) {
710                 $config = new object();
711             }
712             $this->config = $config;
713         }
714     }
716     /**
717      * Returns plugin config value
718      * @param  string $name
719      * @param  string $default value if config does not exist yet
720      * @return string value or default
721      */
722     public function get_config($name, $default = NULL) {
723         $this->load_config();
724         return isset($this->config->$name) ? $this->config->$name : $default;
725     }
727     /**
728      * Sets plugin config value
729      * @param  string $name name of config
730      * @param  string $value string config value, null means delete
731      * @return string value
732      */
733     public function set_config($name, $value) {
734         $pluginname = $this->get_name();
735         $this->load_config();
736         if ($value === NULL) {
737             unset($this->config->$name);
738         } else {
739             $this->config->$name = $value;
740         }
741         set_config($name, $value, "enrol_$pluginname");
742     }
744     /**
745      * Does this plugin assign protected roles are can they be manually removed?
746      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
747      */
748     public function roles_protected() {
749         return true;
750     }
752     /**
753      * Does this plugin allow manual enrolments?
754      *
755      * @param stdClass $instance course enrol instance
756      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
757      *
758      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, trues means nobody may add more enrolments manually
759      */
760     public function allow_enrol(stdClass $instance) {
761         return false;
762     }
764     /**
765      * Does this plugin allow manual unenrolments?
766      *
767      * @param stdClass $instance course enrol instance
768      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
769      *
770      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
771      */
772     public function allow_unenrol(stdClass $instance) {
773         return false;
774     }
776     /**
777      * Does this plugin allow manual changes in user_enrolments table?
778      *
779      * All plugins allowing this must implement 'enrol/xxx:manage' capability
780      *
781      * @param stdClass $instance course enrol instance
782      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
783      */
784     public function allow_manage(stdClass $instance) {
785         return false;
786     }
788     /**
789      * Attempt to automatically enrol current user in course without any interaction,
790      * calling code has to make sure the plugin and instance are active.
791      *
792      * @param stdClass $instance course enrol instance
793      * @param stdClass $user record
794      * @return bool|int false means not enrolled, integer means timeend
795      */
796     public function try_autoenrol(stdClass $instance) {
797         global $USER;
799         return false;
800     }
802     /**
803      * Attempt to automatically gain temporary guest access to course,
804      * calling code has to make sure the plugin and instance are active.
805      *
806      * @param stdClass $instance course enrol instance
807      * @param stdClass $user record
808      * @return bool|int false means no guest access, integer means timeend
809      */
810     public function try_guestaccess(stdClass $instance) {
811         global $USER;
813         return false;
814     }
816     /**
817      * Enrol user into course via enrol instance.
818      *
819      * @param stdClass $instance
820      * @param int $userid
821      * @param int $roleid optional role id
822      * @param int $timestart
823      * @param int $timeend
824      * @return void
825      */
826     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
827         global $DB, $USER, $CFG; // CFG necessary!!!
829         if ($instance->courseid == SITEID) {
830             throw new coding_exception('invalid attempt to enrol into frontpage course!');
831         }
833         $name = $this->get_name();
834         $courseid = $instance->courseid;
836         if ($instance->enrol !== $name) {
837             throw new coding_exception('invalid enrol instance!');
838         }
839         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
841         $inserted = false;
842         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
843             if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
844                 $ue->timestart    = $timestart;
845                 $ue->timeend      = $timeend;
846                 $ue->modifier     = $USER->id;
847                 $ue->timemodified = time();
848                 $DB->update_record('user_enrolments', $ue);
849             }
850         } else {
851             $ue = new object();
852             $ue->enrolid      = $instance->id;
853             $ue->status       = ENROL_USER_ACTIVE;
854             $ue->userid       = $userid;
855             $ue->timestart    = $timestart;
856             $ue->timeend      = $timeend;
857             $ue->modifier     = $USER->id;
858             $ue->timemodified = time();
859             $ue->id = $DB->insert_record('user_enrolments', $ue);
861             $inserted = true;
862         }
864         if ($roleid) {
865             if ($this->roles_protected()) {
866                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
867             } else {
868                 role_assign($roleid, $userid, $context->id);
869             }
870         }
872         if ($inserted) {
873             // add extra info and trigger event
874             $ue->courseid  = $courseid;
875             $ue->enrol     = $name;
876             events_trigger('user_enrolled', $ue);
877         }
879         // reset primitive require_login() caching
880         if ($userid == $USER->id) {
881             if (isset($USER->enrol['enrolled'][$courseid])) {
882                 unset($USER->enrol['enrolled'][$courseid]);
883             }
884             if (isset($USER->enrol['tempguest'][$courseid])) {
885                 unset($USER->enrol['tempguest'][$courseid]);
886                 $USER->access = remove_temp_roles($context, $USER->access);
887             }
888         }
889     }
891     /**
892      * Store user_enrolments changes and trigger event.
893      *
894      * @param object $ue
895      * @param int $user id
896      * @param int $status
897      * @param int $timestart
898      * @param int $timeend
899      * @return void
900      */
901     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
902         global $DB, $USER;
904         $name = $this->get_name();
906         if ($instance->enrol !== $name) {
907             throw new coding_exception('invalid enrol instance!');
908         }
910         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
911             // weird, user not enrolled
912             return;
913         }
915         $modified = false;
916         if (isset($status) and $ue->status != $status) {
917             $ue->status = $status;
918             $modified = true;
919         }
920         if (isset($timestart) and $ue->timestart != $timestart) {
921             $ue->timestart = $timestart;
922             $modified = true;
923         }
924         if (isset($timeend) and $ue->timeend != $timeend) {
925             $ue->timeend = $timeend;
926             $modified = true;
927         }
929         if (!$modified) {
930             // no change
931             return;
932         }
934         $ue->modifierid = $USER->id;
935         $DB->update_record('user_enrolments', $ue);
937         // trigger event
938         $ue->courseid  = $instance->courseid;
939         $ue->enrol     = $instance->name;
940         events_trigger('user_unenrol_modified', $ue);
941     }
943     /**
944      * Unenrol user from course,
945      * the last unenrolment removes all remaining roles.
946      *
947      * @param stdClass $instance
948      * @param int $userid
949      * @return void
950      */
951     public function unenrol_user(stdClass $instance, $userid) {
952         global $CFG, $USER, $DB;
954         $name = $this->get_name();
955         $courseid = $instance->courseid;
957         if ($instance->enrol !== $name) {
958             throw new coding_exception('invalid enrol instance!');
959         }
960         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
962         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
963             // weird, user not enrolled
964             return;
965         }
967         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
968         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
970         // add extra info and trigger event
971         $ue->courseid  = $courseid;
972         $ue->enrol     = $name;
974         $sql = "SELECT 'x'
975                   FROM {user_enrolments} ue
976                   JOIN {enrol} e ON (e.id = ue.enrolid)
977                   WHERE ue.userid = :userid AND e.courseid = :courseid";
978         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
979             $ue->lastenrol = false;
980             events_trigger('user_unenrolled', $ue);
981             // user still has some enrolments, no big cleanup yet
982         } else {
983             // the big cleanup IS necessary!
985             require_once("$CFG->dirroot/group/lib.php");
986             require_once("$CFG->libdir/gradelib.php");
988             // remove all remaining roles
989             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
991             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
992             groups_delete_group_members($courseid, $userid);
994             grade_user_unenrol($courseid, $userid);
996             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
998             $ue->lastenrol = false;
999             events_trigger('user_unenrolled', $ue);
1000         }
1001         // reset primitive require_login() caching
1002         if ($userid == $USER->id) {
1003             if (isset($USER->enrol['enrolled'][$courseid])) {
1004                 unset($USER->enrol['enrolled'][$courseid]);
1005             }
1006             if (isset($USER->enrol['tempguest'][$courseid])) {
1007                 unset($USER->enrol['tempguest'][$courseid]);
1008                 $USER->access = remove_temp_roles($context, $USER->access);
1009             }
1010         }
1011     }
1013     /**
1014      * Forces synchronisation of user enrolments.
1015      *
1016      * This is important especially for external enrol plugins,
1017      * this function is called for all enabled enrol plugins
1018      * right after every user login.
1019      *
1020      * @param object $user user record
1021      * @return void
1022      */
1023     public function sync_user_enrolments($user) {
1024         // override if necessary
1025     }
1027     /**
1028      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1029      * @param int $courseid
1030      * @return moodle_url page url
1031      */
1032     public function get_candidate_link($courseid) {
1033         // override for most plugins, check if instance already exists in cases only one instance is supported
1034         return NULL;
1035     }
1037     /**
1038      * Is it possible to delete enrol instance via standard UI?
1039      *
1040      * @param object $instance
1041      * @return bool
1042      */
1043     public function instance_deleteable($instance) {
1044         return true;
1045     }
1047     /**
1048      * Returns link to manual enrol UI if exists.
1049      * Does the access control tests automatically.
1050      *
1051      * @param object $instance
1052      * @return moodle_url
1053      */
1054     public function get_manual_enrol_link($instance) {
1055         return NULL;
1056     }
1058     /**
1059      * Returns list of unenrol links for all enrol instances in course.
1060      *
1061      * @param int $courseid
1062      * @return moodle_url
1063      */
1064     public function get_unenrolself_link($instance) {
1065         global $USER, $CFG, $DB;
1067         $name = $this->get_name();
1068         if ($instance->enrol !== $name) {
1069             throw new coding_exception('invalid enrol instance!');
1070         }
1072         if ($instance->courseid == SITEID) {
1073             return NULL;
1074         }
1076         if (!enrol_is_enabled($name)) {
1077             return NULL;
1078         }
1080         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1081             return NULL;
1082         }
1084         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1085         $courseid = $instance->courseid;
1087         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1088             return NULL;
1089         }
1091         $context = get_context_instance(CONTEXT_COURSE, $courseid);
1092         if (!has_capability("enrol/$name:unenrolself", $context)) {
1093             return NULL;
1094         }
1096         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1097             return NULL;
1098         }
1100         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1101     }
1103     /**
1104      * Adds enrol instance UI to course edit form
1105      *
1106      * @param object $instance enrol instance or null if does not exist yet
1107      * @param MoodleQuickForm $mform
1108      * @param object $data
1109      * @param object $context context of existing course or parent category if course does not exist
1110      * @return void
1111      */
1112     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1113         // override - usually at least enable/disable switch, has to add own form header
1114     }
1116     /**
1117      * Validates course edit form data
1118      *
1119      * @param object $instance enrol instance or null if does not exist yet
1120      * @param array $data
1121      * @param object $context context of existing course or parent category if course does not exist
1122      * @return array errors array
1123      */
1124     public function course_edit_validation($instance, array $data, $context) {
1125         return array();
1126     }
1128     /**
1129      * Called after updating/inserting course.
1130      *
1131      * @param bool $inserted true if course just inserted
1132      * @param object $course
1133      * @param object $data form data
1134      * @return void
1135      */
1136     public function course_updated($inserted, $course, $data) {
1137         // override if settings on course edit page or some automatic sync needed
1138     }
1140     /**
1141      * Add new instance of enrol plugin settings.
1142      * @param object $course
1143      * @param array instance fields
1144      * @return int id of new instance
1145      */
1146     public function add_instance($course, array $fields = NULL) {
1147         global $DB;
1149         if ($course->id == SITEID) {
1150             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1151         }
1153         $instance = new object();
1154         $instance->enrol          = $this->get_name();
1155         $instance->status         = ENROL_INSTANCE_ENABLED;
1156         $instance->courseid       = $course->id;
1157         $instance->enrolstartdate = 0;
1158         $instance->enrolenddate   = 0;
1159         $instance->timemodified   = time();
1160         $instance->timecreated    = $instance->timemodified;
1161         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1163         $fields = (array)$fields;
1164         unset($fields['enrol']);
1165         unset($fields['courseid']);
1166         unset($fields['sortorder']);
1167         foreach($fields as $field=>$value) {
1168             $instance->$field = $value;
1169         }
1171         return $DB->insert_record('enrol', $instance);
1172     }
1174     /**
1175      * Add new instance of enrol plugin with default settings,
1176      * called when adding new instance manually or when adding new course.
1177      *
1178      * Not all plugins support this.
1179      *
1180      * @param object $course
1181      * @return int id of new instance or null if no default supported
1182      */
1183     public function add_default_instance($course) {
1184         return null;
1185     }
1187     /**
1188      * Delete course enrol plugin instance, unenrol all users.
1189      * @param object $instance
1190      * @return void
1191      */
1192     public function delete_instance($instance) {
1193         global $DB;
1195         $name = $this->get_name();
1196         if ($instance->enrol !== $name) {
1197             throw new coding_exception('invalid enrol instance!');
1198         }
1200         //first unenrol all users
1201         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1202         foreach ($participants as $participant) {
1203             $this->unenrol_user($instance, $participant->userid);
1204         }
1205         $participants->close();
1207         // now clean up all remainders that were not removed correctly
1208         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1209         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1211         // finally drop the enrol row
1212         $DB->delete_records('enrol', array('id'=>$instance->id));
1213     }
1215     /**
1216      * Creates course enrol form, checks if form submitted
1217      * and enrols user if necessary. It can also redirect.
1218      *
1219      * @param stdClass $instance
1220      * @return string html text, usually a form in a text box
1221      */
1222     public function enrol_page_hook(stdClass $instance) {
1223         return null;
1224     }
1226     /**
1227      * Adds navigation links into course admin block.
1228      *
1229      * By defaults looks for manage links only.
1230      *
1231      * @param navigation_node $instancesnode
1232      * @param object $instance
1233      * @return moodle_url;
1234      */
1235     public function add_course_navigation($instancesnode, stdClass $instance) {
1236         if ($managelink = $this->get_manage_link($instance)) {
1237             $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
1238         }
1239     }
1241     /**
1242      * Returns enrolment instance manage link.
1243      *
1244      * By defaults looks for manage.php file and tests for manage capability.
1245      *
1246      * @param object $instance
1247      * @return moodle_url;
1248      */
1249     public function get_manage_link($instance) {
1250         global $CFG, $DB;
1252         $name = $this->get_name();
1254         if ($instance->enrol !== $name) {
1255              throw new coding_exception('Invalid enrol instance type!');
1256         }
1258         if (!file_exists("$CFG->dirroot/enrol/$name/manage.php")) {
1259             return NULL;
1260         }
1262         if ($instance->courseid == SITEID) {
1263             // no enrolments on the frontpage, only roles there allowed
1264             return NULL;
1265         }
1267         $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
1268         if (!has_capability('enrol/'.$name.':manage', $context)) {
1269             return NULL;
1270         }
1272         return new moodle_url("/enrol/$name/manage.php", array('enrolid'=>$instance->id));
1273     }
1275     /**
1276      * Reads version.php and determines if it is necessary
1277      * to execute the cron job now.
1278      * @return bool
1279      */
1280     public function is_cron_required() {
1281         global $CFG;
1283         $name = $this->get_name();
1284         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1285         $plugin = new object();
1286         include($versionfile);
1287         if (empty($plugin->cron)) {
1288             return false;
1289         }
1290         $lastexecuted = $this->get_config('lastcron', 0);
1291         if ($lastexecuted + $plugin->cron < time()) {
1292             return true;
1293         } else {
1294             return false;
1295         }
1296     }
1298     /**
1299      * Called for all enabled enrol plugins that returned true from is_cron_required().
1300      * @return void
1301      */
1302     public function cron() {
1303     }
1305     /**
1306      * Called when user is about to be deleted
1307      * @param object $user
1308      * @return void
1309      */
1310     public function user_delete($user) {
1311         global $DB;
1313         $sql = "SELECT e.*
1314                   FROM {enrol} e
1315                   JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1316                  WHERE e.enrol = :meta AND ue.userid = :userid";
1317         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1319         $rs = $DB->get_records_recordset($sql, $params);
1320         foreach($rs as $instance) {
1321             $this->unenrol_user($instance, $user->id);
1322         }
1323         $rs->close();
1324     }