MDL-23253 plugin now may indicate if the want "Enrol me" link in navigation
[moodle.git] / lib / enrollib.php
CommitLineData
df997f84
PS
1<?php
2
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/>.
17
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 */
26
27
28/** Course enrol instance enabled. (used in enrol->status) */
29define('ENROL_INSTANCE_ENABLED', 0);
30
31/** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
32define('ENROL_INSTANCE_DISABLED', 1);
33
34/** User is active participant (used in user_enrolments->status)*/
35define('ENROL_USER_ACTIVE', 0);
36
37/** User participation in course is suspended (used in user_enrolments->status) */
38define('ENROL_USER_SUSPENDED', 1);
39
40/** Enrol info is cached for this number of seconds in require_login() */
41define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
42
34121765
PS
43/** When user disappears from external source, the enrolment is completely removed */
44define('ENROL_EXT_REMOVED_UNENROL', 0);
45
46/** When user disappears from external source, the enrolment is kept as is - one way sync */
47define('ENROL_EXT_REMOVED_KEEP', 1);
48
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 */
54define('ENROL_EXT_REMOVED_SUSPEND', 2);
55
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 * */
60define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
61
df997f84
PS
62/**
63 * Returns instances of enrol plugins
64 * @param bool $enable return enabled only
65 * @return array of enrol plugins name=>instance
66 */
67function enrol_get_plugins($enabled) {
68 global $CFG;
69
70 $result = array();
71
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 }
84
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 }
94
95 $result[$plugin] = new $class();
96 }
97
98 return $result;
99}
100
101/**
102 * Returns instance of enrol plugin
103 * @param string $name name of enrol plugin ('manual', 'guest', ...)
104 * @return enrol_plugin
105 */
106function enrol_get_plugin($name) {
107 global $CFG;
108
109 if ($name !== clean_param($name, PARAM_SAFEDIR)) {
110 // ignore malformed plugin names completely
111 return null;
112 }
113
114 $location = "$CFG->dirroot/enrol/$name";
115
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 }
124
125 return new $class();
126}
127
128/**
129 * Returns enrolment instances in given course.
130 * @param int $courseid
131 * @param bool $enabled
132 * @return array of enrol instances
133 */
134function enrol_get_instances($courseid, $enabled) {
135 global $DB, $CFG;
136
137 if (!$enabled) {
138 return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
139 }
140
141 $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
142
79721bd3 143 $enabled = explode(',', $CFG->enrol_plugins_enabled);
df997f84
PS
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 }
155
156 return $result;
157}
158
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 */
165function enrol_is_enabled($enrol) {
166 global $CFG;
167
168 if (empty($CFG->enrol_plugins_enabled)) {
169 return false;
170 }
171 return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
172}
173
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 */
181function enrol_check_plugins($user) {
182 global $CFG;
183
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 }
188
e384d2dc
PS
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 }
194
df997f84
PS
195 static $inprogress = array(); // To prevent this function being called more than once in an invocation
196
197 if (!empty($inprogress[$user->id])) {
198 return;
199 }
200
201 $inprogress[$user->id] = true; // Set the flag
202
203 $enabled = enrol_get_plugins(true);
204
205 foreach($enabled as $enrol) {
206 $enrol->sync_user_enrolments($user);
207 }
208
209 unset($inprogress[$user->id]); // Unset the flag
210}
211
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 */
220function 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 }
236}
237
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 */
245function enrol_course_edit_validation(array $data, $context) {
246 $errors = array();
247 $plugins = enrol_get_plugins(true);
248
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 }
263
264 return $errors;
265}
266
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 */
274function enrol_course_updated($inserted, $course, $data) {
275 global $DB, $CFG;
276
277 $plugins = enrol_get_plugins(true);
278
279 foreach ($plugins as $plugin) {
280 $plugin->course_updated($inserted, $course, $data);
281 }
282}
283
284/**
285 * Add navigation nodes
286 * @param navigation_node $coursenode
287 * @param object $course
288 * @return void
289 */
290function enrol_add_course_navigation(navigation_node $coursenode, $course) {
291
292 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
293
294 $instances = enrol_get_instances($course->id, true);
295 $plugins = enrol_get_plugins(true);
296
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 }
303
f5ce6b71 304 $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
df997f84
PS
305
306 if ($course->id != SITEID) {
f5ce6b71 307 // list all participants - allows assigning roles, groups, etc.
df997f84
PS
308 if (has_capability('moodle/course:enrolreview', $coursecontext)) {
309 $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
f5ce6b71 310 $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/users', ''));
df997f84
PS
311 }
312
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 }
f5ce6b71 319 $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
df997f84
PS
320
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 }
328
329 if (!$url) {
330 $instancesnode->trim_if_empty();
331 }
332 }
333
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 }
339
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 }
f5ce6b71 347 $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
df997f84
PS
348
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));
f5ce6b71 353 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/roles', ''));
df997f84
PS
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));
f5ce6b71 359 $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
df997f84
PS
360 }
361 }
362
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));
f5ce6b71 368 $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/roles', ''));
df997f84
PS
369 }
370 }
371
372 // just in case nothing was actually added
373 $usersnode->trim_if_empty();
374
375 if ($course->id != SITEID) {
376 // Unenrol link
217d0397
PS
377 if (is_enrolled($coursecontext)) {
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 break;
386 //TODO. deal with multiple unenrol links - not likely case, but still...
387 }
df997f84 388 }
217d0397
PS
389 } else {
390 if (is_viewing($coursecontext)) {
391 // better not show any enrol link, this is intended for managers and inspectors
392 } else {
393 foreach ($instances as $instance) {
394 if (!isset($plugins[$instance->enrol])) {
395 continue;
396 }
397 $plugin = $plugins[$instance->enrol];
398 if ($plugin->show_enrolme_link($instance)) {
399 $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
400 $coursenode->add(get_string('enrolme', 'core_enrol', format_string($course->shortname)), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
401 break;
402 }
403 }
df997f84
PS
404 }
405 }
df997f84
PS
406 }
407}
408
409/**
410 * Returns list of courses current $USER is enrolled in and can access
411 *
412 * - $fields is an array of field names to ADD
413 * so name the fields you really need, which will
414 * be added and uniq'd
415 *
416 * @param strin|array $fields
417 * @param string $sort
418 * @param int $limit max number of courses
419 * @return array
420 */
421function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
422 global $DB, $USER;
423
424 // Guest account does not have any courses
425 if (isguestuser() or !isloggedin()) {
426 return(array());
427 }
428
429 $basefields = array('id', 'category', 'sortorder',
430 'shortname', 'fullname', 'idnumber',
431 'startdate', 'visible',
432 'groupmode', 'groupmodeforce');
433
434 if (empty($fields)) {
435 $fields = $basefields;
436 } else if (is_string($fields)) {
437 // turn the fields from a string to an array
438 $fields = explode(',', $fields);
439 $fields = array_map('trim', $fields);
440 $fields = array_unique(array_merge($basefields, $fields));
441 } else if (is_array($fields)) {
442 $fields = array_unique(array_merge($basefields, $fields));
443 } else {
444 throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
445 }
446 if (in_array('*', $fields)) {
447 $fields = array('*');
448 }
449
450 $orderby = "";
451 $sort = trim($sort);
452 if (!empty($sort)) {
453 $rawsorts = explode(',', $sort);
454 $sorts = array();
455 foreach ($rawsorts as $rawsort) {
456 $rawsort = trim($rawsort);
457 if (strpos($rawsort, 'c.') === 0) {
458 $rawsort = substr($rawsort, 2);
459 }
460 $sorts[] = trim($rawsort);
461 }
462 $sort = 'c.'.implode(',c.', $sorts);
463 $orderby = "ORDER BY $sort";
464 }
465
466 $wheres = array("c.id <> :siteid");
467 $params = array('siteid'=>SITEID);
468
469 if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
470 // list _only_ this course - anything else is asking for trouble...
471 $wheres[] = "courseid = :loginas";
472 $params['loginas'] = $USER->loginascontext->instanceid;
473 }
474
475 $coursefields = 'c.' .join(',c.', $fields);
476 list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
4129338c 477 $wheres = implode(" AND ", $wheres);
df997f84 478
4129338c
PS
479 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
480 $sql = "SELECT $coursefields $ccselect
df997f84 481 FROM {course} c
4129338c
PS
482 JOIN (SELECT DISTINCT e.courseid
483 FROM {enrol} e
484 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
485 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
486 ) en ON (en.courseid = c.id)
df997f84 487 $ccjoin
4129338c 488 WHERE $wheres
df997f84
PS
489 $orderby";
490 $params['userid'] = $USER->id;
491 $params['active'] = ENROL_USER_ACTIVE;
492 $params['enabled'] = ENROL_INSTANCE_ENABLED;
493 $params['now1'] = round(time(), -2); // improves db caching
494 $params['now2'] = $params['now1'];
495
496 $courses = $DB->get_records_sql($sql, $params, 0, $limit);
497
498 // preload contexts and check visibility
499 foreach ($courses as $id=>$course) {
500 context_instance_preload($course);
501 if (!$course->visible) {
502 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
503 unset($course[$id]);
504 continue;
505 }
506 if (!has_capability('moodle/course:viewhiddencourses', $context)) {
507 unset($course[$id]);
508 continue;
509 }
510 }
511 $courses[$id] = $course;
512 }
513
514 //wow! Is that really all? :-D
515
516 return $courses;
517}
518
519/**
520 * Returns list of courses user is enrolled into.
521 *
522 * - $fields is an array of fieldnames to ADD
523 * so name the fields you really need, which will
524 * be added and uniq'd
525 *
526 * @param int $userid
527 * @param bool $onlyactive return only active enrolments in courses user may see
528 * @param strin|array $fields
529 * @param string $sort
530 * @return array
531 */
532function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
533 global $DB;
534
535 // Guest account does not have any courses
87163782 536 if (isguestuser($userid) or empty($userid)) {
df997f84
PS
537 return(array());
538 }
539
540 $basefields = array('id', 'category', 'sortorder',
541 'shortname', 'fullname', 'idnumber',
542 'startdate', 'visible',
543 'groupmode', 'groupmodeforce');
544
545 if (empty($fields)) {
546 $fields = $basefields;
547 } else if (is_string($fields)) {
548 // turn the fields from a string to an array
549 $fields = explode(',', $fields);
550 $fields = array_map('trim', $fields);
551 $fields = array_unique(array_merge($basefields, $fields));
552 } else if (is_array($fields)) {
553 $fields = array_unique(array_merge($basefields, $fields));
554 } else {
555 throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
556 }
557 if (in_array('*', $fields)) {
558 $fields = array('*');
559 }
560
561 $orderby = "";
562 $sort = trim($sort);
563 if (!empty($sort)) {
564 $rawsorts = explode(',', $sort);
565 $sorts = array();
566 foreach ($rawsorts as $rawsort) {
567 $rawsort = trim($rawsort);
568 if (strpos($rawsort, 'c.') === 0) {
569 $rawsort = substr($rawsort, 2);
570 }
571 $sorts[] = trim($rawsort);
572 }
573 $sort = 'c.'.implode(',c.', $sorts);
574 $orderby = "ORDER BY $sort";
575 }
576
df997f84
PS
577 $params = array('siteid'=>SITEID);
578
579 if ($onlyactive) {
4129338c 580 $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
df997f84
PS
581 $params['now1'] = round(time(), -2); // improves db caching
582 $params['now2'] = $params['now1'];
583 $params['active'] = ENROL_USER_ACTIVE;
584 $params['enabled'] = ENROL_INSTANCE_ENABLED;
4129338c
PS
585 } else {
586 $subwhere = "";
df997f84
PS
587 }
588
589 $coursefields = 'c.' .join(',c.', $fields);
590 list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
df997f84 591
4129338c
PS
592 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
593 $sql = "SELECT $coursefields $ccselect
df997f84 594 FROM {course} c
4129338c
PS
595 JOIN (SELECT DISTINCT e.courseid
596 FROM {enrol} e
597 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
598 $subwhere
599 ) en ON (en.courseid = c.id)
df997f84 600 $ccjoin
4129338c 601 WHERE c.id <> :siteid
df997f84 602 $orderby";
87163782 603 $params['userid'] = $userid;
df997f84
PS
604
605 $courses = $DB->get_records_sql($sql, $params);
606
607 // preload contexts and check visibility
608 foreach ($courses as $id=>$course) {
609 context_instance_preload($course);
610 if ($onlyactive) {
611 if (!$course->visible) {
612 if (!$context = get_context_instance(CONTEXT_COURSE, $id)) {
613 unset($course[$id]);
614 continue;
615 }
616 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
617 unset($course[$id]);
618 continue;
619 }
620 }
621 }
622 $courses[$id] = $course;
623 }
624
625 //wow! Is that really all? :-D
626
627 return $courses;
628
629}
630
631/**
632 * Called when user is about to be deleted.
633 * @param object $user
634 * @return void
635 */
636function enrol_user_delete($user) {
637 global $DB;
638
639 $plugins = enrol_get_plugins(true);
640 foreach ($plugins as $plugin) {
641 $plugin->user_delete($user);
642 }
643
644 // force cleanup of all broken enrolments
645 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
646}
647
648/**
649 * Try to enrol user via default internal auth plugin.
650 *
651 * For now this is always using the manual enrol plugin...
652 *
653 * @param $courseid
654 * @param $userid
655 * @param $roleid
656 * @param $timestart
657 * @param $timeend
658 * @return bool success
659 */
660function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
661 global $DB;
662
663 //note: this is hardcoded to manual plugin for now
664
665 if (!enrol_is_enabled('manual')) {
666 return false;
667 }
668
669 if (!$enrol = enrol_get_plugin('manual')) {
670 return false;
671 }
672 if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
673 return false;
674 }
675 $instance = reset($instances);
676
677 $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
678
679 return true;
680}
681
682/**
683 * All enrol plugins should be based on this class,
684 * this is also the main source of documentation.
685 */
686abstract class enrol_plugin {
687 protected $config = null;
688
689 /**
690 * Returns name of this enrol plugin
691 * @return string
692 */
693 public function get_name() {
694 // second word in class is always enrol name
695 $words = explode('_', get_class($this));
696 return $words[1];
697 }
698
699 /**
700 * Returns localised name of enrol instance
701 *
702 * @param object $instance (null is accepted too)
703 * @return string
704 */
705 public function get_instance_name($instance) {
706 if (empty($instance->name)) {
707 $enrol = $this->get_name();
708 return get_string('pluginname', 'enrol_'.$enrol);
709 } else {
710 return format_string($instance->name);
711 }
712 }
713
714 /**
715 * Makes sure config is loaded and cached.
716 * @return void
717 */
718 protected function load_config() {
719 if (!isset($this->config)) {
720 $name = $this->get_name();
721 if (!$config = get_config("enrol_$name")) {
722 $config = new object();
723 }
724 $this->config = $config;
725 }
726 }
727
728 /**
729 * Returns plugin config value
730 * @param string $name
731 * @param string $default value if config does not exist yet
732 * @return string value or default
733 */
734 public function get_config($name, $default = NULL) {
735 $this->load_config();
736 return isset($this->config->$name) ? $this->config->$name : $default;
737 }
738
739 /**
740 * Sets plugin config value
741 * @param string $name name of config
742 * @param string $value string config value, null means delete
743 * @return string value
744 */
745 public function set_config($name, $value) {
47811589 746 $pluginname = $this->get_name();
df997f84
PS
747 $this->load_config();
748 if ($value === NULL) {
749 unset($this->config->$name);
750 } else {
751 $this->config->$name = $value;
752 }
47811589 753 set_config($name, $value, "enrol_$pluginname");
df997f84
PS
754 }
755
756 /**
757 * Does this plugin assign protected roles are can they be manually removed?
758 * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
759 */
760 public function roles_protected() {
761 return true;
762 }
763
91b99e80
PS
764 /**
765 * Does this plugin allow manual enrolments?
766 *
767 * @param stdClass $instance course enrol instance
768 * All plugins allowing this must implement 'enrol/xxx:enrol' capability
769 *
770 * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, trues means nobody may add more enrolments manually
771 */
772 public function allow_enrol(stdClass $instance) {
773 return false;
774 }
775
df997f84
PS
776 /**
777 * Does this plugin allow manual unenrolments?
778 *
779 * @param stdClass $instance course enrol instance
91b99e80 780 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
df997f84 781 *
91b99e80 782 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
df997f84
PS
783 */
784 public function allow_unenrol(stdClass $instance) {
785 return false;
786 }
787
788 /**
789 * Does this plugin allow manual changes in user_enrolments table?
790 *
91b99e80 791 * All plugins allowing this must implement 'enrol/xxx:manage' capability
df997f84
PS
792 *
793 * @param stdClass $instance course enrol instance
794 * @return bool - true means it is possible to change enrol period and status in user_enrolments table
795 */
796 public function allow_manage(stdClass $instance) {
797 return false;
798 }
799
217d0397
PS
800 /**
801 * Does this plugin support some way to user to self enrol?
802 *
803 * @param stdClass $instance course enrol instance
804 *
805 * @return bool - true means show "Enrol me in this course" link in course UI
806 */
807 public function show_enrolme_link(stdClass $instance) {
808 return false;
809 }
810
df997f84
PS
811 /**
812 * Attempt to automatically enrol current user in course without any interaction,
813 * calling code has to make sure the plugin and instance are active.
814 *
815 * @param stdClass $instance course enrol instance
816 * @param stdClass $user record
817 * @return bool|int false means not enrolled, integer means timeend
818 */
819 public function try_autoenrol(stdClass $instance) {
820 global $USER;
821
822 return false;
823 }
824
825 /**
826 * Attempt to automatically gain temporary guest access to course,
827 * calling code has to make sure the plugin and instance are active.
828 *
829 * @param stdClass $instance course enrol instance
830 * @param stdClass $user record
831 * @return bool|int false means no guest access, integer means timeend
832 */
833 public function try_guestaccess(stdClass $instance) {
834 global $USER;
835
836 return false;
837 }
838
839 /**
840 * Enrol user into course via enrol instance.
841 *
842 * @param stdClass $instance
843 * @param int $userid
844 * @param int $roleid optional role id
845 * @param int $timestart
846 * @param int $timeend
847 * @return void
848 */
849 public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
850 global $DB, $USER, $CFG; // CFG necessary!!!
851
852 if ($instance->courseid == SITEID) {
853 throw new coding_exception('invalid attempt to enrol into frontpage course!');
854 }
855
856 $name = $this->get_name();
857 $courseid = $instance->courseid;
858
859 if ($instance->enrol !== $name) {
860 throw new coding_exception('invalid enrol instance!');
861 }
862 $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
863
864 $inserted = false;
865 if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
866 if ($ue->timestart != $timestart or $ue->timeend != $timeend) {
867 $ue->timestart = $timestart;
868 $ue->timeend = $timeend;
869 $ue->modifier = $USER->id;
870 $ue->timemodified = time();
871 $DB->update_record('user_enrolments', $ue);
872 }
873 } else {
874 $ue = new object();
875 $ue->enrolid = $instance->id;
876 $ue->status = ENROL_USER_ACTIVE;
877 $ue->userid = $userid;
878 $ue->timestart = $timestart;
879 $ue->timeend = $timeend;
880 $ue->modifier = $USER->id;
881 $ue->timemodified = time();
882 $ue->id = $DB->insert_record('user_enrolments', $ue);
883
884 $inserted = true;
885 }
886
887 if ($roleid) {
888 if ($this->roles_protected()) {
889 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
890 } else {
891 role_assign($roleid, $userid, $context->id);
892 }
893 }
894
895 if ($inserted) {
896 // add extra info and trigger event
897 $ue->courseid = $courseid;
898 $ue->enrol = $name;
899 events_trigger('user_enrolled', $ue);
900 }
901
902 // reset primitive require_login() caching
903 if ($userid == $USER->id) {
904 if (isset($USER->enrol['enrolled'][$courseid])) {
905 unset($USER->enrol['enrolled'][$courseid]);
906 }
907 if (isset($USER->enrol['tempguest'][$courseid])) {
908 unset($USER->enrol['tempguest'][$courseid]);
909 $USER->access = remove_temp_roles($context, $USER->access);
910 }
911 }
912 }
913
914 /**
915 * Store user_enrolments changes and trigger event.
916 *
917 * @param object $ue
918 * @param int $user id
919 * @param int $status
920 * @param int $timestart
921 * @param int $timeend
922 * @return void
923 */
924 public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
925 global $DB, $USER;
926
927 $name = $this->get_name();
928
929 if ($instance->enrol !== $name) {
930 throw new coding_exception('invalid enrol instance!');
931 }
932
933 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
934 // weird, user not enrolled
935 return;
936 }
937
938 $modified = false;
939 if (isset($status) and $ue->status != $status) {
940 $ue->status = $status;
941 $modified = true;
942 }
943 if (isset($timestart) and $ue->timestart != $timestart) {
944 $ue->timestart = $timestart;
945 $modified = true;
946 }
947 if (isset($timeend) and $ue->timeend != $timeend) {
948 $ue->timeend = $timeend;
949 $modified = true;
950 }
951
952 if (!$modified) {
953 // no change
954 return;
955 }
956
957 $ue->modifierid = $USER->id;
958 $DB->update_record('user_enrolments', $ue);
959
960 // trigger event
961 $ue->courseid = $instance->courseid;
962 $ue->enrol = $instance->name;
963 events_trigger('user_unenrol_modified', $ue);
964 }
965
966 /**
967 * Unenrol user from course,
968 * the last unenrolment removes all remaining roles.
969 *
970 * @param stdClass $instance
971 * @param int $userid
972 * @return void
973 */
974 public function unenrol_user(stdClass $instance, $userid) {
975 global $CFG, $USER, $DB;
976
977 $name = $this->get_name();
978 $courseid = $instance->courseid;
979
980 if ($instance->enrol !== $name) {
981 throw new coding_exception('invalid enrol instance!');
982 }
983 $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
984
985 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
986 // weird, user not enrolled
987 return;
988 }
989
990 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
991 $DB->delete_records('user_enrolments', array('id'=>$ue->id));
992
993 // add extra info and trigger event
994 $ue->courseid = $courseid;
995 $ue->enrol = $name;
996
997 $sql = "SELECT 'x'
998 FROM {user_enrolments} ue
999 JOIN {enrol} e ON (e.id = ue.enrolid)
1000 WHERE ue.userid = :userid AND e.courseid = :courseid";
1001 if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1002 $ue->lastenrol = false;
1003 events_trigger('user_unenrolled', $ue);
1004 // user still has some enrolments, no big cleanup yet
1005 } else {
1006 // the big cleanup IS necessary!
1007
1008 require_once("$CFG->dirroot/group/lib.php");
1009 require_once("$CFG->libdir/gradelib.php");
1010
1011 // remove all remaining roles
1012 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1013
1014 //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1015 groups_delete_group_members($courseid, $userid);
1016
1017 grade_user_unenrol($courseid, $userid);
1018
1019 $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1020
1021 $ue->lastenrol = false;
1022 events_trigger('user_unenrolled', $ue);
1023 }
1024 // reset primitive require_login() caching
1025 if ($userid == $USER->id) {
1026 if (isset($USER->enrol['enrolled'][$courseid])) {
1027 unset($USER->enrol['enrolled'][$courseid]);
1028 }
1029 if (isset($USER->enrol['tempguest'][$courseid])) {
1030 unset($USER->enrol['tempguest'][$courseid]);
1031 $USER->access = remove_temp_roles($context, $USER->access);
1032 }
1033 }
1034 }
1035
1036 /**
1037 * Forces synchronisation of user enrolments.
1038 *
1039 * This is important especially for external enrol plugins,
1040 * this function is called for all enabled enrol plugins
1041 * right after every user login.
1042 *
1043 * @param object $user user record
1044 * @return void
1045 */
1046 public function sync_user_enrolments($user) {
1047 // override if necessary
1048 }
1049
1050 /**
1051 * Returns link to page which may be used to add new instance of enrolment plugin in course.
1052 * @param int $courseid
1053 * @return moodle_url page url
1054 */
1055 public function get_candidate_link($courseid) {
1056 // override for most plugins, check if instance already exists in cases only one instance is supported
1057 return NULL;
1058 }
1059
1060 /**
1061 * Is it possible to delete enrol instance via standard UI?
1062 *
1063 * @param object $instance
1064 * @return bool
1065 */
1066 public function instance_deleteable($instance) {
1067 return true;
1068 }
1069
1070 /**
1071 * Returns link to manual enrol UI if exists.
1072 * Does the access control tests automatically.
1073 *
1074 * @param object $instance
1075 * @return moodle_url
1076 */
1077 public function get_manual_enrol_link($instance) {
1078 return NULL;
1079 }
1080
1081 /**
1082 * Returns list of unenrol links for all enrol instances in course.
1083 *
217d0397
PS
1084 * @param int $instance
1085 * @return moodle_url or NULL if self unernolmnet not supported
df997f84
PS
1086 */
1087 public function get_unenrolself_link($instance) {
1088 global $USER, $CFG, $DB;
1089
1090 $name = $this->get_name();
1091 if ($instance->enrol !== $name) {
1092 throw new coding_exception('invalid enrol instance!');
1093 }
1094
1095 if ($instance->courseid == SITEID) {
1096 return NULL;
1097 }
1098
1099 if (!enrol_is_enabled($name)) {
1100 return NULL;
1101 }
1102
1103 if ($instance->status != ENROL_INSTANCE_ENABLED) {
1104 return NULL;
1105 }
1106
df997f84
PS
1107 if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1108 return NULL;
1109 }
1110
217d0397
PS
1111 $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1112
df997f84
PS
1113 if (!has_capability("enrol/$name:unenrolself", $context)) {
1114 return NULL;
1115 }
1116
1117 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1118 return NULL;
1119 }
1120
1121 return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1122 }
1123
1124 /**
1125 * Adds enrol instance UI to course edit form
1126 *
1127 * @param object $instance enrol instance or null if does not exist yet
1128 * @param MoodleQuickForm $mform
1129 * @param object $data
1130 * @param object $context context of existing course or parent category if course does not exist
1131 * @return void
1132 */
1133 public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1134 // override - usually at least enable/disable switch, has to add own form header
1135 }
1136
1137 /**
1138 * Validates course edit form data
1139 *
1140 * @param object $instance enrol instance or null if does not exist yet
1141 * @param array $data
1142 * @param object $context context of existing course or parent category if course does not exist
1143 * @return array errors array
1144 */
1145 public function course_edit_validation($instance, array $data, $context) {
1146 return array();
1147 }
1148
1149 /**
1150 * Called after updating/inserting course.
1151 *
1152 * @param bool $inserted true if course just inserted
1153 * @param object $course
1154 * @param object $data form data
1155 * @return void
1156 */
1157 public function course_updated($inserted, $course, $data) {
1158 // override if settings on course edit page or some automatic sync needed
1159 }
1160
1161 /**
1162 * Add new instance of enrol plugin settings.
1163 * @param object $course
1164 * @param array instance fields
1165 * @return int id of new instance
1166 */
1167 public function add_instance($course, array $fields = NULL) {
1168 global $DB;
1169
1170 if ($course->id == SITEID) {
1171 throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1172 }
1173
1174 $instance = new object();
1175 $instance->enrol = $this->get_name();
1176 $instance->status = ENROL_INSTANCE_ENABLED;
1177 $instance->courseid = $course->id;
1178 $instance->enrolstartdate = 0;
1179 $instance->enrolenddate = 0;
1180 $instance->timemodified = time();
1181 $instance->timecreated = $instance->timemodified;
1182 $instance->sortorder = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1183
1184 $fields = (array)$fields;
1185 unset($fields['enrol']);
1186 unset($fields['courseid']);
1187 unset($fields['sortorder']);
1188 foreach($fields as $field=>$value) {
1189 $instance->$field = $value;
1190 }
1191
1192 return $DB->insert_record('enrol', $instance);
1193 }
1194
1195 /**
1196 * Add new instance of enrol plugin with default settings,
1197 * called when adding new instance manually or when adding new course.
1198 *
1199 * Not all plugins support this.
1200 *
1201 * @param object $course
1202 * @return int id of new instance or null if no default supported
1203 */
1204 public function add_default_instance($course) {
1205 return null;
1206 }
1207
1208 /**
1209 * Delete course enrol plugin instance, unenrol all users.
1210 * @param object $instance
1211 * @return void
1212 */
1213 public function delete_instance($instance) {
1214 global $DB;
1215
1216 $name = $this->get_name();
1217 if ($instance->enrol !== $name) {
1218 throw new coding_exception('invalid enrol instance!');
1219 }
1220
1221 //first unenrol all users
1222 $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1223 foreach ($participants as $participant) {
1224 $this->unenrol_user($instance, $participant->userid);
1225 }
1226 $participants->close();
1227
1228 // now clean up all remainders that were not removed correctly
1229 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>$name));
1230 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1231
1232 // finally drop the enrol row
1233 $DB->delete_records('enrol', array('id'=>$instance->id));
1234 }
1235
1236 /**
1237 * Creates course enrol form, checks if form submitted
1238 * and enrols user if necessary. It can also redirect.
1239 *
1240 * @param stdClass $instance
1241 * @return string html text, usually a form in a text box
1242 */
1243 public function enrol_page_hook(stdClass $instance) {
1244 return null;
1245 }
1246
1247 /**
1248 * Adds navigation links into course admin block.
1249 *
1250 * By defaults looks for manage links only.
1251 *
1252 * @param navigation_node $instancesnode
1253 * @param object $instance
1254 * @return moodle_url;
1255 */
1256 public function add_course_navigation($instancesnode, stdClass $instance) {
1257 if ($managelink = $this->get_manage_link($instance)) {
1258 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
1259 }
1260 }
1261
1262 /**
1263 * Returns enrolment instance manage link.
1264 *
1265 * By defaults looks for manage.php file and tests for manage capability.
1266 *
1267 * @param object $instance
1268 * @return moodle_url;
1269 */
1270 public function get_manage_link($instance) {
1271 global $CFG, $DB;
1272
1273 $name = $this->get_name();
1274
1275 if ($instance->enrol !== $name) {
1276 throw new coding_exception('Invalid enrol instance type!');
1277 }
1278
1279 if (!file_exists("$CFG->dirroot/enrol/$name/manage.php")) {
1280 return NULL;
1281 }
1282
1283 if ($instance->courseid == SITEID) {
1284 // no enrolments on the frontpage, only roles there allowed
1285 return NULL;
1286 }
1287
1288 $context = get_context_instance(CONTEXT_COURSE, $instance->courseid);
1289 if (!has_capability('enrol/'.$name.':manage', $context)) {
1290 return NULL;
1291 }
1292
1293 return new moodle_url("/enrol/$name/manage.php", array('enrolid'=>$instance->id));
1294 }
1295
1296 /**
1297 * Reads version.php and determines if it is necessary
1298 * to execute the cron job now.
1299 * @return bool
1300 */
1301 public function is_cron_required() {
1302 global $CFG;
1303
1304 $name = $this->get_name();
1305 $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1306 $plugin = new object();
1307 include($versionfile);
1308 if (empty($plugin->cron)) {
1309 return false;
1310 }
1311 $lastexecuted = $this->get_config('lastcron', 0);
1312 if ($lastexecuted + $plugin->cron < time()) {
1313 return true;
1314 } else {
1315 return false;
1316 }
1317 }
1318
1319 /**
1320 * Called for all enabled enrol plugins that returned true from is_cron_required().
1321 * @return void
1322 */
1323 public function cron() {
1324 }
1325
1326 /**
1327 * Called when user is about to be deleted
1328 * @param object $user
1329 * @return void
1330 */
1331 public function user_delete($user) {
1332 global $DB;
1333
1334 $sql = "SELECT e.*
1335 FROM {enrol} e
1336 JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1337 WHERE e.enrol = :meta AND ue.userid = :userid";
1338 $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1339
1340 $rs = $DB->get_records_recordset($sql, $params);
1341 foreach($rs as $instance) {
1342 $this->unenrol_user($instance, $user->id);
1343 }
1344 $rs->close();
1345 }
1346}
1347