MDL-23502 refactoring towards separate instance config forms
[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 *
78bfb562
PS
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
df997f84
PS
26 */
27
78bfb562 28defined('MOODLE_INTERNAL') || die();
df997f84
PS
29
30/** Course enrol instance enabled. (used in enrol->status) */
31define('ENROL_INSTANCE_ENABLED', 0);
32
33/** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
34define('ENROL_INSTANCE_DISABLED', 1);
35
36/** User is active participant (used in user_enrolments->status)*/
37define('ENROL_USER_ACTIVE', 0);
38
39/** User participation in course is suspended (used in user_enrolments->status) */
40define('ENROL_USER_SUSPENDED', 1);
41
42/** Enrol info is cached for this number of seconds in require_login() */
43define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
44
34121765
PS
45/** When user disappears from external source, the enrolment is completely removed */
46define('ENROL_EXT_REMOVED_UNENROL', 0);
47
48/** When user disappears from external source, the enrolment is kept as is - one way sync */
49define('ENROL_EXT_REMOVED_KEEP', 1);
50
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 */
56define('ENROL_EXT_REMOVED_SUSPEND', 2);
57
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 * */
62define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
63
df997f84
PS
64/**
65 * Returns instances of enrol plugins
66 * @param bool $enable return enabled only
67 * @return array of enrol plugins name=>instance
68 */
69function enrol_get_plugins($enabled) {
70 global $CFG;
71
72 $result = array();
73
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 }
86
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 }
96
97 $result[$plugin] = new $class();
98 }
99
100 return $result;
101}
102
103/**
104 * Returns instance of enrol plugin
105 * @param string $name name of enrol plugin ('manual', 'guest', ...)
106 * @return enrol_plugin
107 */
108function enrol_get_plugin($name) {
109 global $CFG;
110
111 if ($name !== clean_param($name, PARAM_SAFEDIR)) {
112 // ignore malformed plugin names completely
113 return null;
114 }
115
116 $location = "$CFG->dirroot/enrol/$name";
117
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 }
126
127 return new $class();
128}
129
130/**
131 * Returns enrolment instances in given course.
132 * @param int $courseid
133 * @param bool $enabled
134 * @return array of enrol instances
135 */
136function enrol_get_instances($courseid, $enabled) {
137 global $DB, $CFG;
138
139 if (!$enabled) {
140 return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
141 }
142
143 $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
144
79721bd3 145 $enabled = explode(',', $CFG->enrol_plugins_enabled);
df997f84
PS
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 }
157
158 return $result;
159}
160
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 */
167function enrol_is_enabled($enrol) {
168 global $CFG;
169
170 if (empty($CFG->enrol_plugins_enabled)) {
171 return false;
172 }
173 return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
174}
175
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 */
183function enrol_check_plugins($user) {
184 global $CFG;
185
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 }
190
e384d2dc
PS
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 }
196
df997f84
PS
197 static $inprogress = array(); // To prevent this function being called more than once in an invocation
198
199 if (!empty($inprogress[$user->id])) {
200 return;
201 }
202
203 $inprogress[$user->id] = true; // Set the flag
204
205 $enabled = enrol_get_plugins(true);
206
207 foreach($enabled as $enrol) {
208 $enrol->sync_user_enrolments($user);
209 }
210
211 unset($inprogress[$user->id]); // Unset the flag
212}
213
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 */
222function 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 }
238}
239
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 */
247function enrol_course_edit_validation(array $data, $context) {
248 $errors = array();
249 $plugins = enrol_get_plugins(true);
250
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 }
265
266 return $errors;
267}
268
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 */
276function enrol_course_updated($inserted, $course, $data) {
277 global $DB, $CFG;
278
279 $plugins = enrol_get_plugins(true);
280
281 foreach ($plugins as $plugin) {
282 $plugin->course_updated($inserted, $course, $data);
283 }
284}
285
286/**
287 * Add navigation nodes
288 * @param navigation_node $coursenode
289 * @param object $course
290 * @return void
291 */
292function enrol_add_course_navigation(navigation_node $coursenode, $course) {
293
294 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
295
296 $instances = enrol_get_instances($course->id, true);
297 $plugins = enrol_get_plugins(true);
298
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 }
305
f5ce6b71 306 $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
df997f84
PS
307
308 if ($course->id != SITEID) {
f5ce6b71 309 // list all participants - allows assigning roles, groups, etc.
df997f84
PS
310 if (has_capability('moodle/course:enrolreview', $coursecontext)) {
311 $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
f5ce6b71 312 $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/users', ''));
df997f84
PS
313 }
314
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 }
f5ce6b71 321 $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
df997f84
PS
322
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 }
330
331 if (!$url) {
332 $instancesnode->trim_if_empty();
333 }
334 }
335
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 }
341
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 }
f5ce6b71 349 $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
df997f84
PS
350
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));
f5ce6b71 355 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/roles', ''));
df997f84
PS
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));
f5ce6b71 361 $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
df997f84
PS
362 }
363 }
364
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));
f5ce6b71 370 $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/roles', ''));
df997f84
PS
371 }
372 }
373
374 // just in case nothing was actually added
375 $usersnode->trim_if_empty();
376
377 if ($course->id != SITEID) {
378 // Unenrol link
217d0397
PS
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 }
df997f84 390 }
217d0397
PS
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 }
df997f84
PS
406 }
407 }
df997f84
PS
408 }
409}
410
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 */
423function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
424 global $DB, $USER;
425
426 // Guest account does not have any courses
427 if (isguestuser() or !isloggedin()) {
428 return(array());
429 }
430
431 $basefields = array('id', 'category', 'sortorder',
432 'shortname', 'fullname', 'idnumber',
433 'startdate', 'visible',
434 'groupmode', 'groupmodeforce');
435
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 }
451
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 }
467
468 $wheres = array("c.id <> :siteid");
469 $params = array('siteid'=>SITEID);
470
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 }
476
477 $coursefields = 'c.' .join(',c.', $fields);
478 list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
4129338c 479 $wheres = implode(" AND ", $wheres);
df997f84 480
4129338c
PS
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
df997f84 483 FROM {course} c
4129338c
PS
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)
df997f84 489 $ccjoin
4129338c 490 WHERE $wheres
df997f84
PS
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'];
497
498 $courses = $DB->get_records_sql($sql, $params, 0, $limit);
499
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)) {
55880bdd 505 unset($courses[$id]);
df997f84
PS
506 continue;
507 }
508 if (!has_capability('moodle/course:viewhiddencourses', $context)) {
55880bdd 509 unset($courses[$id]);
df997f84
PS
510 continue;
511 }
512 }
513 $courses[$id] = $course;
514 }
515
516 //wow! Is that really all? :-D
517
518 return $courses;
519}
520
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 */
534function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
535 global $DB;
536
537 // Guest account does not have any courses
87163782 538 if (isguestuser($userid) or empty($userid)) {
df997f84
PS
539 return(array());
540 }
541
542 $basefields = array('id', 'category', 'sortorder',
543 'shortname', 'fullname', 'idnumber',
544 'startdate', 'visible',
545 'groupmode', 'groupmodeforce');
546
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 }
562
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 }
578
df997f84
PS
579 $params = array('siteid'=>SITEID);
580
581 if ($onlyactive) {
4129338c 582 $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
df997f84
PS
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;
4129338c
PS
587 } else {
588 $subwhere = "";
df997f84
PS
589 }
590
591 $coursefields = 'c.' .join(',c.', $fields);
592 list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
df997f84 593
4129338c
PS
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
df997f84 596 FROM {course} c
4129338c
PS
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)
df997f84 602 $ccjoin
4129338c 603 WHERE c.id <> :siteid
df997f84 604 $orderby";
87163782 605 $params['userid'] = $userid;
df997f84
PS
606
607 $courses = $DB->get_records_sql($sql, $params);
608
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)) {
4a5aba7c 615 unset($courses[$id]);
df997f84
PS
616 continue;
617 }
618 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
4a5aba7c 619 unset($courses[$id]);
df997f84
PS
620 continue;
621 }
622 }
623 }
624 $courses[$id] = $course;
625 }
626
627 //wow! Is that really all? :-D
628
629 return $courses;
630
631}
632
633/**
634 * Called when user is about to be deleted.
635 * @param object $user
636 * @return void
637 */
638function enrol_user_delete($user) {
639 global $DB;
640
641 $plugins = enrol_get_plugins(true);
642 foreach ($plugins as $plugin) {
643 $plugin->user_delete($user);
644 }
645
646 // force cleanup of all broken enrolments
647 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
648}
649
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 */
662function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
663 global $DB;
664
665 //note: this is hardcoded to manual plugin for now
666
667 if (!enrol_is_enabled('manual')) {
668 return false;
669 }
670
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);
678
679 $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
680
681 return true;
682}
683
684/**
685 * All enrol plugins should be based on this class,
686 * this is also the main source of documentation.
687 */
688abstract class enrol_plugin {
689 protected $config = null;
690
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 }
700
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 }
715
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 }
729
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 }
740
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) {
47811589 748 $pluginname = $this->get_name();
df997f84
PS
749 $this->load_config();
750 if ($value === NULL) {
751 unset($this->config->$name);
752 } else {
753 $this->config->$name = $value;
754 }
47811589 755 set_config($name, $value, "enrol_$pluginname");
df997f84
PS
756 }
757
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 }
765
91b99e80
PS
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 }
777
df997f84
PS
778 /**
779 * Does this plugin allow manual unenrolments?
780 *
781 * @param stdClass $instance course enrol instance
91b99e80 782 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
df997f84 783 *
91b99e80 784 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, trues means nobody may touch user_enrolments
df997f84
PS
785 */
786 public function allow_unenrol(stdClass $instance) {
787 return false;
788 }
789
790 /**
791 * Does this plugin allow manual changes in user_enrolments table?
792 *
91b99e80 793 * All plugins allowing this must implement 'enrol/xxx:manage' capability
df997f84
PS
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 }
801
217d0397
PS
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 }
812
df997f84
PS
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;
823
824 return false;
825 }
826
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;
837
838 return false;
839 }
840
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
2a6dcb72
PS
847 * @param int $timestart 0 means unknown
848 * @param int $timeend 0 means forever
df997f84
PS
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!!!
853
854 if ($instance->courseid == SITEID) {
855 throw new coding_exception('invalid attempt to enrol into frontpage course!');
856 }
857
858 $name = $this->get_name();
859 $courseid = $instance->courseid;
860
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);
865
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;
2a6dcb72
PS
883 $ue->timecreated = time();
884 $ue->timemodified = $ue->timecreated;
df997f84
PS
885 $ue->id = $DB->insert_record('user_enrolments', $ue);
886
887 $inserted = true;
888 }
889
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 }
897
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 }
904
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 }
916
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;
929
930 $name = $this->get_name();
931
932 if ($instance->enrol !== $name) {
933 throw new coding_exception('invalid enrol instance!');
934 }
935
936 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
937 // weird, user not enrolled
938 return;
939 }
940
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 }
954
955 if (!$modified) {
956 // no change
957 return;
958 }
959
960 $ue->modifierid = $USER->id;
961 $DB->update_record('user_enrolments', $ue);
962
963 // trigger event
964 $ue->courseid = $instance->courseid;
965 $ue->enrol = $instance->name;
966 events_trigger('user_unenrol_modified', $ue);
967 }
968
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;
979
980 $name = $this->get_name();
981 $courseid = $instance->courseid;
982
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);
987
988 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
989 // weird, user not enrolled
990 return;
991 }
992
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));
995
996 // add extra info and trigger event
997 $ue->courseid = $courseid;
998 $ue->enrol = $name;
999
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!
1010
1011 require_once("$CFG->dirroot/group/lib.php");
1012 require_once("$CFG->libdir/gradelib.php");
1013
1014 // remove all remaining roles
1015 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1016
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);
1019
1020 grade_user_unenrol($courseid, $userid);
1021
1022 $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1023
7d2800fb 1024 $ue->lastenrol = true; // means user not enrolled any more
df997f84
PS
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 }
1038
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 }
1052
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 }
1062
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 }
1072
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 }
1083
1084 /**
1085 * Returns list of unenrol links for all enrol instances in course.
1086 *
217d0397
PS
1087 * @param int $instance
1088 * @return moodle_url or NULL if self unernolmnet not supported
df997f84
PS
1089 */
1090 public function get_unenrolself_link($instance) {
1091 global $USER, $CFG, $DB;
1092
1093 $name = $this->get_name();
1094 if ($instance->enrol !== $name) {
1095 throw new coding_exception('invalid enrol instance!');
1096 }
1097
1098 if ($instance->courseid == SITEID) {
1099 return NULL;
1100 }
1101
1102 if (!enrol_is_enabled($name)) {
1103 return NULL;
1104 }
1105
1106 if ($instance->status != ENROL_INSTANCE_ENABLED) {
1107 return NULL;
1108 }
1109
df997f84
PS
1110 if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1111 return NULL;
1112 }
1113
217d0397
PS
1114 $context = get_context_instance(CONTEXT_COURSE, $instance->courseid, MUST_EXIST);
1115
df997f84
PS
1116 if (!has_capability("enrol/$name:unenrolself", $context)) {
1117 return NULL;
1118 }
1119
1120 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1121 return NULL;
1122 }
1123
1124 return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));;
1125 }
1126
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 }
1139
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 }
1151
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 }
1163
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;
1172
1173 if ($course->id == SITEID) {
1174 throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1175 }
1176
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));
1186
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 }
1194
1195 return $DB->insert_record('enrol', $instance);
1196 }
1197
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 }
1210
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;
1218
1219 $name = $this->get_name();
1220 if ($instance->enrol !== $name) {
1221 throw new coding_exception('invalid enrol instance!');
1222 }
1223
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();
1230
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));
1234
1235 // finally drop the enrol row
1236 $DB->delete_records('enrol', array('id'=>$instance->id));
1237 }
1238
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 }
1249
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
2d4b1f3e 1257 * @return void
df997f84
PS
1258 */
1259 public function add_course_navigation($instancesnode, stdClass $instance) {
2d4b1f3e 1260 // usually adds manage users
df997f84
PS
1261 }
1262
1263 /**
2d4b1f3e
PS
1264 * Returns edit icons for the page with list of instances
1265 * @param stdClass $instance
1266 * @return array
df997f84 1267 */
2d4b1f3e
PS
1268 public function get_action_icons(stdClass $instance) {
1269 return array();
df997f84
PS
1270 }
1271
1272 /**
1273 * Reads version.php and determines if it is necessary
1274 * to execute the cron job now.
1275 * @return bool
1276 */
1277 public function is_cron_required() {
1278 global $CFG;
1279
1280 $name = $this->get_name();
1281 $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1282 $plugin = new object();
1283 include($versionfile);
1284 if (empty($plugin->cron)) {
1285 return false;
1286 }
1287 $lastexecuted = $this->get_config('lastcron', 0);
1288 if ($lastexecuted + $plugin->cron < time()) {
1289 return true;
1290 } else {
1291 return false;
1292 }
1293 }
1294
1295 /**
1296 * Called for all enabled enrol plugins that returned true from is_cron_required().
1297 * @return void
1298 */
1299 public function cron() {
1300 }
1301
1302 /**
1303 * Called when user is about to be deleted
1304 * @param object $user
1305 * @return void
1306 */
1307 public function user_delete($user) {
1308 global $DB;
1309
1310 $sql = "SELECT e.*
1311 FROM {enrol} e
1312 JOIN {user_enrolments} ue ON (ue.courseid = e.courseid)
1313 WHERE e.enrol = :meta AND ue.userid = :userid";
1314 $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1315
1316 $rs = $DB->get_records_recordset($sql, $params);
1317 foreach($rs as $instance) {
1318 $this->unenrol_user($instance, $user->id);
1319 }
1320 $rs->close();
1321 }
1322}
1323