MDL-59369 enrol: Introduce data-action attribute for enrol action links
[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
bbfdff34 42/** @deprecated - enrol caching was reworked, use ENROL_MAX_TIMESTAMP instead */
df997f84
PS
43define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
44
bbfdff34
PS
45/** The timestamp indicating forever */
46define('ENROL_MAX_TIMESTAMP', 2147483647);
47
34121765
PS
48/** When user disappears from external source, the enrolment is completely removed */
49define('ENROL_EXT_REMOVED_UNENROL', 0);
50
51/** When user disappears from external source, the enrolment is kept as is - one way sync */
52define('ENROL_EXT_REMOVED_KEEP', 1);
53
7a7b8a1f 54/** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */
f2a9be5f 55define('ENROL_RESTORE_TYPE', 'enrolrestore');
f2a9be5f 56
34121765
PS
57/**
58 * When user disappears from external source, user enrolment is suspended, roles are kept as is.
59 * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
60 * assignments, etc.
61 */
62define('ENROL_EXT_REMOVED_SUSPEND', 2);
63
64/**
65 * When user disappears from external source, the enrolment is suspended and roles assigned
66 * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
67 * */
68define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
69
0ab8b337
SL
70/**
71 * Do not send email.
72 */
73define('ENROL_DO_NOT_SEND_EMAIL', 0);
74
75/**
76 * Send email from course contact.
77 */
78define('ENROL_SEND_EMAIL_FROM_COURSE_CONTACT', 1);
79
80/**
81 * Send email from enrolment key holder.
82 */
83define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2);
84
85/**
86 * Send email from no reply address.
87 */
88define('ENROL_SEND_EMAIL_FROM_NOREPLY', 3);
89
fd0a43be
JP
90/** Edit enrolment action. */
91define('ENROL_ACTION_EDIT', 'editenrolment');
92
93/** Unenrol action. */
94define('ENROL_ACTION_UNENROL', 'unenrol');
95
df997f84
PS
96/**
97 * Returns instances of enrol plugins
bbfdff34 98 * @param bool $enabled return enabled only
df997f84
PS
99 * @return array of enrol plugins name=>instance
100 */
101function enrol_get_plugins($enabled) {
102 global $CFG;
103
104 $result = array();
105
106 if ($enabled) {
107 // sorted by enabled plugin order
108 $enabled = explode(',', $CFG->enrol_plugins_enabled);
109 $plugins = array();
110 foreach ($enabled as $plugin) {
111 $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
112 }
113 } else {
114 // sorted alphabetically
bd3b3bba 115 $plugins = core_component::get_plugin_list('enrol');
df997f84
PS
116 ksort($plugins);
117 }
118
119 foreach ($plugins as $plugin=>$location) {
df997f84
PS
120 $class = "enrol_{$plugin}_plugin";
121 if (!class_exists($class)) {
d432e0f7
MA
122 if (!file_exists("$location/lib.php")) {
123 continue;
124 }
125 include_once("$location/lib.php");
126 if (!class_exists($class)) {
127 continue;
128 }
df997f84
PS
129 }
130
131 $result[$plugin] = new $class();
132 }
133
134 return $result;
135}
136
137/**
138 * Returns instance of enrol plugin
139 * @param string $name name of enrol plugin ('manual', 'guest', ...)
140 * @return enrol_plugin
141 */
142function enrol_get_plugin($name) {
143 global $CFG;
144
aff24313
PS
145 $name = clean_param($name, PARAM_PLUGIN);
146
147 if (empty($name)) {
148 // ignore malformed or missing plugin names completely
df997f84
PS
149 return null;
150 }
151
152 $location = "$CFG->dirroot/enrol/$name";
153
df997f84
PS
154 $class = "enrol_{$name}_plugin";
155 if (!class_exists($class)) {
ef97f1a2
MA
156 if (!file_exists("$location/lib.php")) {
157 return null;
158 }
159 include_once("$location/lib.php");
160 if (!class_exists($class)) {
161 return null;
162 }
df997f84
PS
163 }
164
165 return new $class();
166}
167
168/**
169 * Returns enrolment instances in given course.
170 * @param int $courseid
171 * @param bool $enabled
172 * @return array of enrol instances
173 */
174function enrol_get_instances($courseid, $enabled) {
175 global $DB, $CFG;
176
177 if (!$enabled) {
178 return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
179 }
180
181 $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
182
79721bd3 183 $enabled = explode(',', $CFG->enrol_plugins_enabled);
df997f84
PS
184 foreach ($result as $key=>$instance) {
185 if (!in_array($instance->enrol, $enabled)) {
186 unset($result[$key]);
187 continue;
188 }
189 if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
190 // broken plugin
191 unset($result[$key]);
192 continue;
193 }
194 }
195
196 return $result;
197}
198
199/**
200 * Checks if a given plugin is in the list of enabled enrolment plugins.
201 *
202 * @param string $enrol Enrolment plugin name
203 * @return boolean Whether the plugin is enabled
204 */
205function enrol_is_enabled($enrol) {
206 global $CFG;
207
208 if (empty($CFG->enrol_plugins_enabled)) {
209 return false;
210 }
211 return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
212}
213
214/**
215 * Check all the login enrolment information for the given user object
216 * by querying the enrolment plugins
217 *
e922fe23
PS
218 * This function may be very slow, use only once after log-in or login-as.
219 *
220 * @param stdClass $user
df997f84
PS
221 * @return void
222 */
223function enrol_check_plugins($user) {
224 global $CFG;
225
226 if (empty($user->id) or isguestuser($user)) {
227 // shortcut - there is no enrolment work for guests and not-logged-in users
228 return;
229 }
230
bcb368d9
PS
231 // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
232 // which proved it was actually not necessary.
e384d2dc 233
df997f84
PS
234 static $inprogress = array(); // To prevent this function being called more than once in an invocation
235
236 if (!empty($inprogress[$user->id])) {
237 return;
238 }
239
240 $inprogress[$user->id] = true; // Set the flag
241
242 $enabled = enrol_get_plugins(true);
243
244 foreach($enabled as $enrol) {
245 $enrol->sync_user_enrolments($user);
246 }
247
248 unset($inprogress[$user->id]); // Unset the flag
249}
250
181991e7
PS
251/**
252 * Do these two students share any course?
253 *
254 * The courses has to be visible and enrolments has to be active,
255 * timestart and timeend restrictions are ignored.
256 *
61ab8f07
SH
257 * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
258 * to true.
259 *
181991e7
PS
260 * @param stdClass|int $user1
261 * @param stdClass|int $user2
262 * @return bool
263 */
264function enrol_sharing_course($user1, $user2) {
61ab8f07 265 return enrol_get_shared_courses($user1, $user2, false, true);
181991e7
PS
266}
267
4b715423
SH
268/**
269 * Returns any courses shared by the two users
270 *
271 * The courses has to be visible and enrolments has to be active,
272 * timestart and timeend restrictions are ignored.
273 *
61ab8f07 274 * @global moodle_database $DB
4b715423
SH
275 * @param stdClass|int $user1
276 * @param stdClass|int $user2
61ab8f07
SH
277 * @param bool $preloadcontexts If set to true contexts for the returned courses
278 * will be preloaded.
279 * @param bool $checkexistsonly If set to true then this function will return true
280 * if the users share any courses and false if not.
281 * @return array|bool An array of courses that both users are enrolled in OR if
282 * $checkexistsonly set returns true if the users share any courses
283 * and false if not.
4b715423 284 */
61ab8f07 285function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
4b715423
SH
286 global $DB, $CFG;
287
5b244e2b
HD
288 $user1 = isset($user1->id) ? $user1->id : $user1;
289 $user2 = isset($user2->id) ? $user2->id : $user2;
4b715423
SH
290
291 if (empty($user1) or empty($user2)) {
292 return false;
293 }
294
295 if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
296 return false;
297 }
298
499d3775 299 list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
300 list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
301 $params = array_merge($params1, $params2);
302 $params['enabled1'] = ENROL_INSTANCE_ENABLED;
303 $params['enabled2'] = ENROL_INSTANCE_ENABLED;
4b715423
SH
304 $params['active1'] = ENROL_USER_ACTIVE;
305 $params['active2'] = ENROL_USER_ACTIVE;
306 $params['user1'] = $user1;
307 $params['user2'] = $user2;
308
309 $ctxselect = '';
310 $ctxjoin = '';
311 if ($preloadcontexts) {
2e4c0c91
FM
312 $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
313 $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
314 $params['contextlevel'] = CONTEXT_COURSE;
4b715423
SH
315 }
316
317 $sql = "SELECT c.* $ctxselect
318 FROM {course} c
319 JOIN (
320 SELECT DISTINCT c.id
499d3775 321 FROM {course} c
322 JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
323 JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
324 JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
325 JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
326 WHERE c.visible = 1
4b715423
SH
327 ) ec ON ec.id = c.id
328 $ctxjoin";
4b715423 329
61ab8f07
SH
330 if ($checkexistsonly) {
331 return $DB->record_exists_sql($sql, $params);
332 } else {
333 $courses = $DB->get_records_sql($sql, $params);
334 if ($preloadcontexts) {
a55ad8f8 335 array_map('context_helper::preload_from_record', $courses);
61ab8f07
SH
336 }
337 return $courses;
338 }
4b715423
SH
339}
340
df997f84
PS
341/**
342 * This function adds necessary enrol plugins UI into the course edit form.
343 *
344 * @param MoodleQuickForm $mform
345 * @param object $data course edit form data
346 * @param object $context context of existing course or parent category if course does not exist
347 * @return void
348 */
349function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
350 $plugins = enrol_get_plugins(true);
351 if (!empty($data->id)) {
352 $instances = enrol_get_instances($data->id, false);
353 foreach ($instances as $instance) {
354 if (!isset($plugins[$instance->enrol])) {
355 continue;
356 }
357 $plugin = $plugins[$instance->enrol];
358 $plugin->course_edit_form($instance, $mform, $data, $context);
359 }
360 } else {
361 foreach ($plugins as $plugin) {
362 $plugin->course_edit_form(NULL, $mform, $data, $context);
363 }
364 }
365}
366
367/**
368 * Validate course edit form data
369 *
370 * @param array $data raw form data
371 * @param object $context context of existing course or parent category if course does not exist
372 * @return array errors array
373 */
374function enrol_course_edit_validation(array $data, $context) {
375 $errors = array();
376 $plugins = enrol_get_plugins(true);
377
378 if (!empty($data['id'])) {
379 $instances = enrol_get_instances($data['id'], false);
380 foreach ($instances as $instance) {
381 if (!isset($plugins[$instance->enrol])) {
382 continue;
383 }
384 $plugin = $plugins[$instance->enrol];
385 $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
386 }
387 } else {
388 foreach ($plugins as $plugin) {
389 $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
390 }
391 }
392
393 return $errors;
394}
395
396/**
397 * Update enrol instances after course edit form submission
398 * @param bool $inserted true means new course added, false course already existed
399 * @param object $course
400 * @param object $data form data
401 * @return void
402 */
403function enrol_course_updated($inserted, $course, $data) {
404 global $DB, $CFG;
405
406 $plugins = enrol_get_plugins(true);
407
408 foreach ($plugins as $plugin) {
409 $plugin->course_updated($inserted, $course, $data);
410 }
411}
412
413/**
414 * Add navigation nodes
415 * @param navigation_node $coursenode
416 * @param object $course
417 * @return void
418 */
419function enrol_add_course_navigation(navigation_node $coursenode, $course) {
6e4c374d 420 global $CFG;
df997f84 421
b0c6dc1c 422 $coursecontext = context_course::instance($course->id);
df997f84
PS
423
424 $instances = enrol_get_instances($course->id, true);
425 $plugins = enrol_get_plugins(true);
426
427 // we do not want to break all course pages if there is some borked enrol plugin, right?
428 foreach ($instances as $k=>$instance) {
429 if (!isset($plugins[$instance->enrol])) {
430 unset($instances[$k]);
431 }
432 }
433
f5ce6b71 434 $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
df997f84
PS
435
436 if ($course->id != SITEID) {
f5ce6b71 437 // list all participants - allows assigning roles, groups, etc.
df997f84
PS
438 if (has_capability('moodle/course:enrolreview', $coursecontext)) {
439 $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
c42651d6 440 $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
df997f84
PS
441 }
442
443 // manage enrol plugin instances
444 if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
445 $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
446 } else {
447 $url = NULL;
448 }
f5ce6b71 449 $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
df997f84
PS
450
451 // each instance decides how to configure itself or how many other nav items are exposed
452 foreach ($instances as $instance) {
453 if (!isset($plugins[$instance->enrol])) {
454 continue;
455 }
456 $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
457 }
458
459 if (!$url) {
460 $instancesnode->trim_if_empty();
461 }
462 }
463
464 // Manage groups in this course or even frontpage
465 if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
466 $url = new moodle_url('/group/index.php', array('id'=>$course->id));
467 $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
468 }
469
470 if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
471 // Override roles
472 if (has_capability('moodle/role:review', $coursecontext)) {
473 $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
474 } else {
475 $url = NULL;
476 }
f5ce6b71 477 $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
df997f84
PS
478
479 // Add assign or override roles if allowed
480 if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
481 if (has_capability('moodle/role:assign', $coursecontext)) {
482 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
fbb207c5 483 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
df997f84
PS
484 }
485 }
486 // Check role permissions
77690b69 487 if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
df997f84 488 $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
f5ce6b71 489 $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
df997f84
PS
490 }
491 }
492
493 // Deal somehow with users that are not enrolled but still got a role somehow
494 if ($course->id != SITEID) {
495 //TODO, create some new UI for role assignments at course level
288e7b09 496 if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
df997f84 497 $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
fbb207c5 498 $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
df997f84
PS
499 }
500 }
501
502 // just in case nothing was actually added
503 $usersnode->trim_if_empty();
504
505 if ($course->id != SITEID) {
6813cdf7 506 if (isguestuser() or !isloggedin()) {
39ec18db 507 // guest account can not be enrolled - no links for them
6813cdf7 508 } else if (is_enrolled($coursecontext)) {
39ec18db 509 // unenrol link if possible
217d0397
PS
510 foreach ($instances as $instance) {
511 if (!isset($plugins[$instance->enrol])) {
512 continue;
513 }
514 $plugin = $plugins[$instance->enrol];
515 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
8ebbb06a
SH
516 $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
517 $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
217d0397
PS
518 break;
519 //TODO. deal with multiple unenrol links - not likely case, but still...
520 }
df997f84 521 }
217d0397 522 } else {
39ec18db 523 // enrol link if possible
217d0397
PS
524 if (is_viewing($coursecontext)) {
525 // better not show any enrol link, this is intended for managers and inspectors
526 } else {
527 foreach ($instances as $instance) {
528 if (!isset($plugins[$instance->enrol])) {
529 continue;
530 }
531 $plugin = $plugins[$instance->enrol];
532 if ($plugin->show_enrolme_link($instance)) {
533 $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
8ebbb06a
SH
534 $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
535 $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
217d0397
PS
536 break;
537 }
538 }
df997f84
PS
539 }
540 }
df997f84
PS
541 }
542}
543
544/**
545 * Returns list of courses current $USER is enrolled in and can access
546 *
547 * - $fields is an array of field names to ADD
548 * so name the fields you really need, which will
549 * be added and uniq'd
550 *
eb6f592a 551 * @param string|array $fields
df997f84
PS
552 * @param string $sort
553 * @param int $limit max number of courses
1ef06b43 554 * @param array $courseids the list of course ids to filter by
df997f84
PS
555 * @return array
556 */
966cbed6 557function enrol_get_my_courses($fields = null, $sort = 'visible DESC,sortorder ASC',
1ef06b43 558 $limit = 0, $courseids = []) {
df997f84
PS
559 global $DB, $USER;
560
561 // Guest account does not have any courses
562 if (isguestuser() or !isloggedin()) {
563 return(array());
564 }
565
566 $basefields = array('id', 'category', 'sortorder',
567 'shortname', 'fullname', 'idnumber',
568 'startdate', 'visible',
4a3fb71c 569 'groupmode', 'groupmodeforce', 'cacherev');
df997f84
PS
570
571 if (empty($fields)) {
572 $fields = $basefields;
573 } else if (is_string($fields)) {
574 // turn the fields from a string to an array
575 $fields = explode(',', $fields);
576 $fields = array_map('trim', $fields);
577 $fields = array_unique(array_merge($basefields, $fields));
578 } else if (is_array($fields)) {
579 $fields = array_unique(array_merge($basefields, $fields));
580 } else {
581 throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
582 }
583 if (in_array('*', $fields)) {
584 $fields = array('*');
585 }
586
587 $orderby = "";
588 $sort = trim($sort);
589 if (!empty($sort)) {
590 $rawsorts = explode(',', $sort);
591 $sorts = array();
592 foreach ($rawsorts as $rawsort) {
593 $rawsort = trim($rawsort);
594 if (strpos($rawsort, 'c.') === 0) {
595 $rawsort = substr($rawsort, 2);
596 }
597 $sorts[] = trim($rawsort);
598 }
599 $sort = 'c.'.implode(',c.', $sorts);
600 $orderby = "ORDER BY $sort";
601 }
602
603 $wheres = array("c.id <> :siteid");
604 $params = array('siteid'=>SITEID);
605
606 if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
607 // list _only_ this course - anything else is asking for trouble...
608 $wheres[] = "courseid = :loginas";
609 $params['loginas'] = $USER->loginascontext->instanceid;
610 }
611
612 $coursefields = 'c.' .join(',c.', $fields);
2e4c0c91
FM
613 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
614 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
615 $params['contextlevel'] = CONTEXT_COURSE;
4129338c 616 $wheres = implode(" AND ", $wheres);
df997f84 617
1ef06b43
RW
618 if (!empty($courseids)) {
619 list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
620 $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
621 $params = array_merge($params, $courseidsparams);
622 }
623
4129338c
PS
624 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
625 $sql = "SELECT $coursefields $ccselect
df997f84 626 FROM {course} c
4129338c
PS
627 JOIN (SELECT DISTINCT e.courseid
628 FROM {enrol} e
629 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
630 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
631 ) en ON (en.courseid = c.id)
df997f84 632 $ccjoin
4129338c 633 WHERE $wheres
df997f84
PS
634 $orderby";
635 $params['userid'] = $USER->id;
636 $params['active'] = ENROL_USER_ACTIVE;
637 $params['enabled'] = ENROL_INSTANCE_ENABLED;
638 $params['now1'] = round(time(), -2); // improves db caching
639 $params['now2'] = $params['now1'];
640
641 $courses = $DB->get_records_sql($sql, $params, 0, $limit);
642
643 // preload contexts and check visibility
644 foreach ($courses as $id=>$course) {
db314f34 645 context_helper::preload_from_record($course);
df997f84 646 if (!$course->visible) {
b0c6dc1c 647 if (!$context = context_course::instance($id, IGNORE_MISSING)) {
55880bdd 648 unset($courses[$id]);
df997f84
PS
649 continue;
650 }
651 if (!has_capability('moodle/course:viewhiddencourses', $context)) {
55880bdd 652 unset($courses[$id]);
df997f84
PS
653 continue;
654 }
655 }
656 $courses[$id] = $course;
657 }
658
659 //wow! Is that really all? :-D
660
661 return $courses;
662}
663
bf423bb1
PS
664/**
665 * Returns course enrolment information icons.
666 *
667 * @param object $course
668 * @param array $instances enrol instances of this course, improves performance
669 * @return array of pix_icon
670 */
671function enrol_get_course_info_icons($course, array $instances = NULL) {
672 $icons = array();
673 if (is_null($instances)) {
674 $instances = enrol_get_instances($course->id, true);
675 }
676 $plugins = enrol_get_plugins(true);
677 foreach ($plugins as $name => $plugin) {
678 $pis = array();
679 foreach ($instances as $instance) {
680 if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
681 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
682 continue;
683 }
684 if ($instance->enrol == $name) {
685 $pis[$instance->id] = $instance;
686 }
687 }
688 if ($pis) {
689 $icons = array_merge($icons, $plugin->get_info_icons($pis));
690 }
691 }
692 return $icons;
693}
694
695/**
696 * Returns course enrolment detailed information.
697 *
698 * @param object $course
699 * @return array of html fragments - can be used to construct lists
700 */
701function enrol_get_course_description_texts($course) {
702 $lines = array();
703 $instances = enrol_get_instances($course->id, true);
704 $plugins = enrol_get_plugins(true);
705 foreach ($instances as $instance) {
64942a9d 706 if (!isset($plugins[$instance->enrol])) {
bf423bb1
PS
707 //weird
708 continue;
709 }
64942a9d 710 $plugin = $plugins[$instance->enrol];
bf423bb1
PS
711 $text = $plugin->get_description_text($instance);
712 if ($text !== NULL) {
713 $lines[] = $text;
714 }
715 }
716 return $lines;
717}
718
df997f84
PS
719/**
720 * Returns list of courses user is enrolled into.
9ffd29ce 721 * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
df997f84
PS
722 *
723 * - $fields is an array of fieldnames to ADD
724 * so name the fields you really need, which will
725 * be added and uniq'd
726 *
727 * @param int $userid
728 * @param bool $onlyactive return only active enrolments in courses user may see
eb6f592a 729 * @param string|array $fields
df997f84
PS
730 * @param string $sort
731 * @return array
732 */
733function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
734 global $DB;
735
9ffd29ce
AA
736 $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
737
738 // preload contexts and check visibility
739 if ($onlyactive) {
740 foreach ($courses as $id=>$course) {
db314f34 741 context_helper::preload_from_record($course);
9ffd29ce
AA
742 if (!$course->visible) {
743 if (!$context = context_course::instance($id)) {
744 unset($courses[$id]);
745 continue;
746 }
747 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
748 unset($courses[$id]);
749 continue;
750 }
751 }
752 }
753 }
754
755 return $courses;
756
757}
758
05f6da14
759/**
760 * Can user access at least one enrolled course?
761 *
762 * Cheat if necessary, but find out as fast as possible!
763 *
764 * @param int|stdClass $user null means use current user
765 * @return bool
766 */
767function enrol_user_sees_own_courses($user = null) {
768 global $USER;
769
770 if ($user === null) {
771 $user = $USER;
772 }
773 $userid = is_object($user) ? $user->id : $user;
774
775 // Guest account does not have any courses
776 if (isguestuser($userid) or empty($userid)) {
777 return false;
778 }
779
780 // Let's cheat here if this is the current user,
781 // if user accessed any course recently, then most probably
782 // we do not need to query the database at all.
783 if ($USER->id == $userid) {
784 if (!empty($USER->enrol['enrolled'])) {
785 foreach ($USER->enrol['enrolled'] as $until) {
786 if ($until > time()) {
787 return true;
788 }
789 }
790 }
791 }
792
793 // Now the slow way.
794 $courses = enrol_get_all_users_courses($userid, true);
795 foreach($courses as $course) {
796 if ($course->visible) {
797 return true;
798 }
799 context_helper::preload_from_record($course);
800 $context = context_course::instance($course->id);
801 if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
802 return true;
803 }
804 }
805
806 return false;
807}
808
9ffd29ce
AA
809/**
810 * Returns list of courses user is enrolled into without any capability checks
811 * - $fields is an array of fieldnames to ADD
812 * so name the fields you really need, which will
813 * be added and uniq'd
814 *
815 * @param int $userid
816 * @param bool $onlyactive return only active enrolments in courses user may see
817 * @param string|array $fields
818 * @param string $sort
819 * @return array
820 */
821function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
822 global $DB;
823
df997f84 824 // Guest account does not have any courses
87163782 825 if (isguestuser($userid) or empty($userid)) {
df997f84
PS
826 return(array());
827 }
828
829 $basefields = array('id', 'category', 'sortorder',
9ffd29ce
AA
830 'shortname', 'fullname', 'idnumber',
831 'startdate', 'visible',
8eb15a38 832 'defaultgroupingid',
9ffd29ce 833 'groupmode', 'groupmodeforce');
df997f84
PS
834
835 if (empty($fields)) {
836 $fields = $basefields;
837 } else if (is_string($fields)) {
838 // turn the fields from a string to an array
839 $fields = explode(',', $fields);
840 $fields = array_map('trim', $fields);
841 $fields = array_unique(array_merge($basefields, $fields));
842 } else if (is_array($fields)) {
843 $fields = array_unique(array_merge($basefields, $fields));
844 } else {
845 throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
846 }
847 if (in_array('*', $fields)) {
848 $fields = array('*');
849 }
850
851 $orderby = "";
852 $sort = trim($sort);
853 if (!empty($sort)) {
854 $rawsorts = explode(',', $sort);
855 $sorts = array();
856 foreach ($rawsorts as $rawsort) {
857 $rawsort = trim($rawsort);
858 if (strpos($rawsort, 'c.') === 0) {
859 $rawsort = substr($rawsort, 2);
860 }
861 $sorts[] = trim($rawsort);
862 }
863 $sort = 'c.'.implode(',c.', $sorts);
864 $orderby = "ORDER BY $sort";
865 }
866
df997f84
PS
867 $params = array('siteid'=>SITEID);
868
869 if ($onlyactive) {
4129338c 870 $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
df997f84
PS
871 $params['now1'] = round(time(), -2); // improves db caching
872 $params['now2'] = $params['now1'];
873 $params['active'] = ENROL_USER_ACTIVE;
874 $params['enabled'] = ENROL_INSTANCE_ENABLED;
4129338c
PS
875 } else {
876 $subwhere = "";
df997f84
PS
877 }
878
879 $coursefields = 'c.' .join(',c.', $fields);
2e4c0c91
FM
880 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
881 $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
882 $params['contextlevel'] = CONTEXT_COURSE;
df997f84 883
4129338c
PS
884 //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
885 $sql = "SELECT $coursefields $ccselect
df997f84 886 FROM {course} c
4129338c
PS
887 JOIN (SELECT DISTINCT e.courseid
888 FROM {enrol} e
889 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
890 $subwhere
891 ) en ON (en.courseid = c.id)
df997f84 892 $ccjoin
4129338c 893 WHERE c.id <> :siteid
df997f84 894 $orderby";
87163782 895 $params['userid'] = $userid;
df997f84
PS
896
897 $courses = $DB->get_records_sql($sql, $params);
898
df997f84 899 return $courses;
df997f84
PS
900}
901
9ffd29ce
AA
902
903
df997f84
PS
904/**
905 * Called when user is about to be deleted.
906 * @param object $user
907 * @return void
908 */
909function enrol_user_delete($user) {
910 global $DB;
911
912 $plugins = enrol_get_plugins(true);
913 foreach ($plugins as $plugin) {
914 $plugin->user_delete($user);
915 }
916
917 // force cleanup of all broken enrolments
918 $DB->delete_records('user_enrolments', array('userid'=>$user->id));
919}
920
582bae08
PS
921/**
922 * Called when course is about to be deleted.
bbfdff34 923 * @param stdClass $course
582bae08
PS
924 * @return void
925 */
926function enrol_course_delete($course) {
927 global $DB;
928
929 $instances = enrol_get_instances($course->id, false);
930 $plugins = enrol_get_plugins(true);
931 foreach ($instances as $instance) {
932 if (isset($plugins[$instance->enrol])) {
933 $plugins[$instance->enrol]->delete_instance($instance);
934 }
935 // low level delete in case plugin did not do it
936 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
937 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
938 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
939 $DB->delete_records('enrol', array('id'=>$instance->id));
940 }
941}
942
df997f84
PS
943/**
944 * Try to enrol user via default internal auth plugin.
945 *
946 * For now this is always using the manual enrol plugin...
947 *
948 * @param $courseid
949 * @param $userid
950 * @param $roleid
951 * @param $timestart
952 * @param $timeend
953 * @return bool success
954 */
955function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
956 global $DB;
957
958 //note: this is hardcoded to manual plugin for now
959
960 if (!enrol_is_enabled('manual')) {
961 return false;
962 }
963
964 if (!$enrol = enrol_get_plugin('manual')) {
965 return false;
966 }
967 if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
968 return false;
969 }
970 $instance = reset($instances);
971
972 $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
973
974 return true;
975}
976
45ff8a80
PS
977/**
978 * Is there a chance users might self enrol
979 * @param int $courseid
980 * @return bool
981 */
982function enrol_selfenrol_available($courseid) {
983 $result = false;
984
985 $plugins = enrol_get_plugins(true);
986 $enrolinstances = enrol_get_instances($courseid, true);
987 foreach($enrolinstances as $instance) {
988 if (!isset($plugins[$instance->enrol])) {
989 continue;
990 }
991 if ($instance->enrol === 'guest') {
992 // blacklist known temporary guest plugins
993 continue;
994 }
995 if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
996 $result = true;
997 break;
998 }
999 }
1000
1001 return $result;
1002}
1003
bbfdff34
PS
1004/**
1005 * This function returns the end of current active user enrolment.
1006 *
1007 * It deals correctly with multiple overlapping user enrolments.
1008 *
1009 * @param int $courseid
1010 * @param int $userid
1011 * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1012 */
1013function enrol_get_enrolment_end($courseid, $userid) {
1014 global $DB;
1015
1016 $sql = "SELECT ue.*
1017 FROM {user_enrolments} ue
1018 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1019 JOIN {user} u ON u.id = ue.userid
1020 WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1021 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1022
1023 if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1024 return false;
1025 }
1026
1027 $changes = array();
1028
1029 foreach ($enrolments as $ue) {
1030 $start = (int)$ue->timestart;
1031 $end = (int)$ue->timeend;
1032 if ($end != 0 and $end < $start) {
1033 debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1034 continue;
1035 }
1036 if (isset($changes[$start])) {
1037 $changes[$start] = $changes[$start] + 1;
1038 } else {
1039 $changes[$start] = 1;
1040 }
1041 if ($end === 0) {
1042 // no end
1043 } else if (isset($changes[$end])) {
1044 $changes[$end] = $changes[$end] - 1;
1045 } else {
1046 $changes[$end] = -1;
1047 }
1048 }
1049
1050 // let's sort then enrolment starts&ends and go through them chronologically,
1051 // looking for current status and the next future end of enrolment
1052 ksort($changes);
1053
1054 $now = time();
1055 $current = 0;
1056 $present = null;
1057
1058 foreach ($changes as $time => $change) {
1059 if ($time > $now) {
1060 if ($present === null) {
1061 // we have just went past current time
1062 $present = $current;
1063 if ($present < 1) {
1064 // no enrolment active
1065 return false;
1066 }
1067 }
1068 if ($present !== null) {
1069 // we are already in the future - look for possible end
1070 if ($current + $change < 1) {
1071 return $time;
1072 }
1073 }
1074 }
1075 $current += $change;
1076 }
1077
1078 if ($current > 0) {
1079 return 0;
1080 } else {
1081 return false;
1082 }
1083}
1084
12c92bca
PS
1085/**
1086 * Is current user accessing course via this enrolment method?
1087 *
1088 * This is intended for operations that are going to affect enrol instances.
1089 *
1090 * @param stdClass $instance enrol instance
1091 * @return bool
1092 */
1093function enrol_accessing_via_instance(stdClass $instance) {
1094 global $DB, $USER;
1095
1096 if (empty($instance->id)) {
1097 return false;
1098 }
1099
1100 if (is_siteadmin()) {
1101 // Admins may go anywhere.
1102 return false;
1103 }
1104
1105 return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1106}
1107
9f5170e9
JB
1108/**
1109 * Returns true if user is enrolled (is participating) in course
1110 * this is intended for students and teachers.
1111 *
1112 * Since 2.2 the result for active enrolments and current user are cached.
1113 *
9f5170e9
JB
1114 * @param context $context
1115 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1116 * @param string $withcapability extra capability name
1117 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1118 * @return bool
1119 */
1120function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1121 global $USER, $DB;
1122
9121bb2d 1123 // First find the course context.
9f5170e9
JB
1124 $coursecontext = $context->get_course_context();
1125
9121bb2d 1126 // Make sure there is a real user specified.
9f5170e9
JB
1127 if ($user === null) {
1128 $userid = isset($USER->id) ? $USER->id : 0;
1129 } else {
1130 $userid = is_object($user) ? $user->id : $user;
1131 }
1132
1133 if (empty($userid)) {
9121bb2d 1134 // Not-logged-in!
9f5170e9
JB
1135 return false;
1136 } else if (isguestuser($userid)) {
9121bb2d 1137 // Guest account can not be enrolled anywhere.
9f5170e9
JB
1138 return false;
1139 }
1140
9121bb2d
JB
1141 // Note everybody participates on frontpage, so for other contexts...
1142 if ($coursecontext->instanceid != SITEID) {
1143 // Try cached info first - the enrolled flag is set only when active enrolment present.
9f5170e9
JB
1144 if ($USER->id == $userid) {
1145 $coursecontext->reload_if_dirty();
1146 if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1147 if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1148 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1149 return false;
1150 }
1151 return true;
1152 }
1153 }
1154 }
1155
1156 if ($onlyactive) {
9121bb2d 1157 // Look for active enrolments only.
9f5170e9
JB
1158 $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1159
1160 if ($until === false) {
1161 return false;
1162 }
1163
1164 if ($USER->id == $userid) {
1165 if ($until == 0) {
1166 $until = ENROL_MAX_TIMESTAMP;
1167 }
1168 $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1169 if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1170 unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1171 remove_temp_course_roles($coursecontext);
1172 }
1173 }
1174
1175 } else {
9121bb2d 1176 // Any enrolment is good for us here, even outdated, disabled or inactive.
9f5170e9
JB
1177 $sql = "SELECT 'x'
1178 FROM {user_enrolments} ue
1179 JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1180 JOIN {user} u ON u.id = ue.userid
1181 WHERE ue.userid = :userid AND u.deleted = 0";
9121bb2d 1182 $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
9f5170e9
JB
1183 if (!$DB->record_exists_sql($sql, $params)) {
1184 return false;
1185 }
1186 }
1187 }
1188
1189 if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1190 return false;
1191 }
1192
1193 return true;
1194}
1195
9121bb2d
JB
1196/**
1197 * Returns an array of joins, wheres and params that will limit the group of
1198 * users to only those enrolled and with given capability (if specified).
1199 *
8b0d254f
JB
1200 * Note this join will return duplicate rows for users who have been enrolled
1201 * several times (e.g. as manual enrolment, and as self enrolment). You may
1202 * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1203 *
9121bb2d
JB
1204 * @param context $context
1205 * @param string $prefix optional, a prefix to the user id column
10c4fce5
JB
1206 * @param string|array $capability optional, may include a capability name, or array of names.
1207 * If an array is provided then this is the equivalent of a logical 'OR',
1208 * i.e. the user needs to have one of these capabilities.
9121bb2d
JB
1209 * @param int $group optional, 0 indicates no current group, otherwise the group id
1210 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1211 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1212 * @return \core\dml\sql_join Contains joins, wheres, params
1213 */
1214function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
1215 $onlyactive = false, $onlysuspended = false) {
1216 $uid = $prefix . 'u.id';
1217 $joins = array();
1218 $wheres = array();
1219
1220 $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended);
1221 $joins[] = $enrolledjoin->joins;
1222 $wheres[] = $enrolledjoin->wheres;
1223 $params = $enrolledjoin->params;
1224
1225 if (!empty($capability)) {
1226 $capjoin = get_with_capability_join($context, $capability, $uid);
1227 $joins[] = $capjoin->joins;
1228 $wheres[] = $capjoin->wheres;
1229 $params = array_merge($params, $capjoin->params);
1230 }
1231
1232 if ($group) {
90595390 1233 $groupjoin = groups_get_members_join($group, $uid);
9121bb2d
JB
1234 $joins[] = $groupjoin->joins;
1235 $params = array_merge($params, $groupjoin->params);
1236 }
1237
1238 $joins = implode("\n", $joins);
1239 $wheres[] = "{$prefix}u.deleted = 0";
1240 $wheres = implode(" AND ", $wheres);
1241
1242 return new \core\dml\sql_join($joins, $wheres, $params);
1243}
1244
9f5170e9
JB
1245/**
1246 * Returns array with sql code and parameters returning all ids
1247 * of users enrolled into course.
1248 *
1249 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1250 *
9f5170e9
JB
1251 * @param context $context
1252 * @param string $withcapability
1253 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1254 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1255 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1256 * @return array list($sql, $params)
1257 */
1258function get_enrolled_sql(context $context, $withcapability = '', $groupid = 0, $onlyactive = false, $onlysuspended = false) {
9f5170e9 1259
9121bb2d 1260 // Use unique prefix just in case somebody makes some SQL magic with the result.
9f5170e9
JB
1261 static $i = 0;
1262 $i++;
9121bb2d 1263 $prefix = 'eu' . $i . '_';
9f5170e9 1264
9121bb2d
JB
1265 $capjoin = get_enrolled_with_capabilities_join(
1266 $context, $prefix, $withcapability, $groupid, $onlyactive, $onlysuspended);
1267
1268 $sql = "SELECT DISTINCT {$prefix}u.id
1269 FROM {user} {$prefix}u
1270 $capjoin->joins
1271 WHERE $capjoin->wheres";
1272
1273 return array($sql, $capjoin->params);
1274}
1275
1276/**
1277 * Returns array with sql joins and parameters returning all ids
1278 * of users enrolled into course.
1279 *
1280 * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1281 *
1282 * @throws coding_exception
1283 *
1284 * @param context $context
1285 * @param string $useridcolumn User id column used the calling query, e.g. u.id
1286 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1287 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1288 * @return \core\dml\sql_join Contains joins, wheres, params
1289 */
1290function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false) {
1291 // Use unique prefix just in case somebody makes some SQL magic with the result.
1292 static $i = 0;
1293 $i++;
1294 $prefix = 'ej' . $i . '_';
1295
1296 // First find the course context.
9f5170e9
JB
1297 $coursecontext = $context->get_course_context();
1298
1299 $isfrontpage = ($coursecontext->instanceid == SITEID);
1300
1301 if ($onlyactive && $onlysuspended) {
1302 throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1303 }
1304 if ($isfrontpage && $onlysuspended) {
1305 throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1306 }
1307
1308 $joins = array();
1309 $wheres = array();
1310 $params = array();
1311
9121bb2d 1312 $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
9f5170e9 1313
9121bb2d
JB
1314 // Note all users are "enrolled" on the frontpage, but for others...
1315 if (!$isfrontpage) {
9f5170e9
JB
1316 $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1317 $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1318 $ejoin = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
1319 $params[$prefix.'courseid'] = $coursecontext->instanceid;
1320
1321 if (!$onlysuspended) {
9121bb2d 1322 $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
9f5170e9
JB
1323 $joins[] = $ejoin;
1324 if ($onlyactive) {
1325 $wheres[] = "$where1 AND $where2";
1326 }
1327 } else {
1328 // Suspended only where there is enrolment but ALL are suspended.
1329 // Consider multiple enrols where one is not suspended or plain role_assign.
1330 $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
9121bb2d
JB
1331 $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1332 $joins[] = "JOIN {enrol} {$prefix}e1 ON ({$prefix}e1.id = {$prefix}ue1.enrolid
1333 AND {$prefix}e1.courseid = :{$prefix}_e1_courseid)";
9f5170e9 1334 $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
9121bb2d 1335 $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
9f5170e9
JB
1336 }
1337
1338 if ($onlyactive || $onlysuspended) {
9121bb2d
JB
1339 $now = round(time(), -2); // Rounding helps caching in DB.
1340 $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1341 $prefix . 'active' => ENROL_USER_ACTIVE,
1342 $prefix . 'now1' => $now, $prefix . 'now2' => $now));
9f5170e9
JB
1343 }
1344 }
1345
1346 $joins = implode("\n", $joins);
9121bb2d 1347 $wheres = implode(" AND ", $wheres);
9f5170e9 1348
9121bb2d 1349 return new \core\dml\sql_join($joins, $wheres, $params);
9f5170e9
JB
1350}
1351
1352/**
1353 * Returns list of users enrolled into course.
1354 *
9f5170e9
JB
1355 * @param context $context
1356 * @param string $withcapability
1357 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1358 * @param string $userfields requested user record fields
1359 * @param string $orderby
1360 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1361 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1362 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1363 * @return array of user records
1364 */
1365function get_enrolled_users(context $context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = null,
1366 $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1367 global $DB;
1368
1369 list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid, $onlyactive);
1370 $sql = "SELECT $userfields
1371 FROM {user} u
1372 JOIN ($esql) je ON je.id = u.id
1373 WHERE u.deleted = 0";
1374
1375 if ($orderby) {
1376 $sql = "$sql ORDER BY $orderby";
1377 } else {
1378 list($sort, $sortparams) = users_order_by_sql('u');
1379 $sql = "$sql ORDER BY $sort";
1380 $params = array_merge($params, $sortparams);
1381 }
1382
1383 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1384}
1385
1386/**
1387 * Counts list of users enrolled into course (as per above function)
1388 *
9f5170e9
JB
1389 * @param context $context
1390 * @param string $withcapability
1391 * @param int $groupid 0 means ignore groups, any other value limits the result by group id
1392 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1393 * @return array of user records
1394 */
1395function count_enrolled_users(context $context, $withcapability = '', $groupid = 0, $onlyactive = false) {
1396 global $DB;
1397
9121bb2d
JB
1398 $capjoin = get_enrolled_with_capabilities_join(
1399 $context, '', $withcapability, $groupid, $onlyactive);
1400
9f5170e9
JB
1401 $sql = "SELECT count(u.id)
1402 FROM {user} u
9121bb2d
JB
1403 $capjoin->joins
1404 WHERE $capjoin->wheres AND u.deleted = 0";
9f5170e9 1405
9121bb2d 1406 return $DB->count_records_sql($sql, $capjoin->params);
9f5170e9 1407}
bbfdff34 1408
0ab8b337
SL
1409/**
1410 * Send welcome email "from" options.
1411 *
1412 * @return array list of from options
1413 */
1414function enrol_send_welcome_email_options() {
1415 return [
1416 ENROL_DO_NOT_SEND_EMAIL => get_string('no'),
1417 ENROL_SEND_EMAIL_FROM_COURSE_CONTACT => get_string('sendfromcoursecontact', 'enrol'),
1418 ENROL_SEND_EMAIL_FROM_KEY_HOLDER => get_string('sendfromkeyholder', 'enrol'),
1419 ENROL_SEND_EMAIL_FROM_NOREPLY => get_string('sendfromnoreply', 'enrol')
1420 ];
1421}
1422
5719f2ca
JP
1423/**
1424 * Serve the user enrolment form as a fragment.
1425 *
1426 * @param array $args List of named arguments for the fragment loader.
1427 * @return string
1428 */
1429function enrol_output_fragment_user_enrolment_form($args) {
1430 global $CFG, $DB;
1431
1432 $args = (object) $args;
1433 $context = $args->context;
1434 require_capability('moodle/course:enrolreview', $context);
1435
1436 $ueid = $args->ueid;
1437 $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1438 $customdata = [
1439 'ue' => $userenrolment,
1440 'modal' => true,
1441 ];
1442
1443 // Set the data if applicable.
1444 $data = [];
1445 if (isset($args->formdata)) {
1446 $serialiseddata = json_decode($args->formdata);
1447 parse_str($serialiseddata, $data);
1448 }
1449
1450 require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1451 $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1452
1453 if (!empty($data)) {
1454 $mform->set_data($data);
1455 $mform->is_validated();
1456 }
1457
1458 return $mform->render();
1459}
1460
fba40526
DM
1461/**
1462 * Returns the course where a user enrolment belong to.
1463 *
1464 * @param int $ueid user_enrolments id
1465 * @return stdClass
1466 */
1467function enrol_get_course_by_user_enrolment_id($ueid) {
1468 global $DB;
1469 $sql = "SELECT c.* FROM {user_enrolments} ue
1470 JOIN {enrol} e ON e.id = ue.enrolid
1471 JOIN {course} c ON c.id = e.courseid
1472 WHERE ue.id = :ueid";
1473 return $DB->get_record_sql($sql, array('ueid' => $ueid));
1474}
1475
8970ff91
DM
1476/**
1477 * Return all users enrolled in a course.
1478 *
58f86c4c 1479 * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
8970ff91 1480 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
413f19bc
DM
1481 * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1482 * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1483 * @return stdClass[]
8970ff91 1484 */
58f86c4c 1485function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
8970ff91
DM
1486 global $DB;
1487
58f86c4c
DM
1488 if (!$courseid && !$usersfilter && !$uefilter) {
1489 throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1490 }
1491
1492 $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1493 ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1494 ue.timemodified AS uetimemodified,
1495 u.* FROM {user_enrolments} ue
8970ff91
DM
1496 JOIN {enrol} e ON e.id = ue.enrolid
1497 JOIN {user} u ON ue.userid = u.id
58f86c4c
DM
1498 WHERE ";
1499 $params = array();
1500
1501 if ($courseid) {
1502 $conditions[] = "e.courseid = :courseid";
1503 $params['courseid'] = $courseid;
1504 }
8970ff91
DM
1505
1506 if ($onlyactive) {
413f19bc
DM
1507 $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1508 "(ue.timeend = 0 OR ue.timeend > :now2)";
1509 // Improves db caching.
1510 $params['now1'] = round(time(), -2);
8970ff91
DM
1511 $params['now2'] = $params['now1'];
1512 $params['active'] = ENROL_USER_ACTIVE;
1513 $params['enabled'] = ENROL_INSTANCE_ENABLED;
8970ff91 1514 }
58f86c4c
DM
1515
1516 if ($usersfilter) {
1517 list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1518 $conditions[] = "ue.userid $usersql";
1519 $params = $params + $userparams;
1520 }
1521
1522 if ($uefilter) {
b3d68794 1523 list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
58f86c4c
DM
1524 $conditions[] = "ue.id $uesql";
1525 $params = $params + $ueparams;
1526 }
1527
1528 return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
8970ff91
DM
1529}
1530
df997f84 1531/**
413f19bc
DM
1532 * Enrolment plugins abstract class.
1533 *
df997f84
PS
1534 * All enrol plugins should be based on this class,
1535 * this is also the main source of documentation.
413f19bc
DM
1536 *
1537 * @copyright 2010 Petr Skoda {@link http://skodak.org}
1538 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
df997f84
PS
1539 */
1540abstract class enrol_plugin {
1541 protected $config = null;
1542
1543 /**
1544 * Returns name of this enrol plugin
1545 * @return string
1546 */
1547 public function get_name() {
bf423bb1 1548 // second word in class is always enrol name, sorry, no fancy plugin names with _
df997f84
PS
1549 $words = explode('_', get_class($this));
1550 return $words[1];
1551 }
1552
1553 /**
1554 * Returns localised name of enrol instance
1555 *
1556 * @param object $instance (null is accepted too)
1557 * @return string
1558 */
1559 public function get_instance_name($instance) {
1560 if (empty($instance->name)) {
1561 $enrol = $this->get_name();
1562 return get_string('pluginname', 'enrol_'.$enrol);
1563 } else {
b0c6dc1c 1564 $context = context_course::instance($instance->courseid);
54475ccb 1565 return format_string($instance->name, true, array('context'=>$context));
df997f84
PS
1566 }
1567 }
1568
bf423bb1
PS
1569 /**
1570 * Returns optional enrolment information icons.
1571 *
1572 * This is used in course list for quick overview of enrolment options.
1573 *
1574 * We are not using single instance parameter because sometimes
1575 * we might want to prevent icon repetition when multiple instances
1576 * of one type exist. One instance may also produce several icons.
1577 *
1578 * @param array $instances all enrol instances of this type in one course
1579 * @return array of pix_icon
1580 */
1581 public function get_info_icons(array $instances) {
1582 return array();
1583 }
1584
1585 /**
1586 * Returns optional enrolment instance description text.
1587 *
1588 * This is used in detailed course information.
1589 *
1590 *
1591 * @param object $instance
1592 * @return string short html text
1593 */
1594 public function get_description_text($instance) {
1595 return null;
1596 }
1597
df997f84
PS
1598 /**
1599 * Makes sure config is loaded and cached.
1600 * @return void
1601 */
1602 protected function load_config() {
1603 if (!isset($this->config)) {
1604 $name = $this->get_name();
820a8188 1605 $this->config = get_config("enrol_$name");
df997f84
PS
1606 }
1607 }
1608
1609 /**
1610 * Returns plugin config value
1611 * @param string $name
1612 * @param string $default value if config does not exist yet
1613 * @return string value or default
1614 */
1615 public function get_config($name, $default = NULL) {
1616 $this->load_config();
1617 return isset($this->config->$name) ? $this->config->$name : $default;
1618 }
1619
1620 /**
1621 * Sets plugin config value
1622 * @param string $name name of config
1623 * @param string $value string config value, null means delete
1624 * @return string value
1625 */
1626 public function set_config($name, $value) {
47811589 1627 $pluginname = $this->get_name();
df997f84
PS
1628 $this->load_config();
1629 if ($value === NULL) {
1630 unset($this->config->$name);
1631 } else {
1632 $this->config->$name = $value;
1633 }
47811589 1634 set_config($name, $value, "enrol_$pluginname");
df997f84
PS
1635 }
1636
1637 /**
1638 * Does this plugin assign protected roles are can they be manually removed?
1639 * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1640 */
1641 public function roles_protected() {
1642 return true;
1643 }
1644
91b99e80
PS
1645 /**
1646 * Does this plugin allow manual enrolments?
1647 *
1648 * @param stdClass $instance course enrol instance
1649 * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1650 *
282b5cc7 1651 * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
91b99e80
PS
1652 */
1653 public function allow_enrol(stdClass $instance) {
1654 return false;
1655 }
1656
df997f84 1657 /**
282b5cc7 1658 * Does this plugin allow manual unenrolment of all users?
91b99e80 1659 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
df997f84 1660 *
282b5cc7
PS
1661 * @param stdClass $instance course enrol instance
1662 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
df997f84
PS
1663 */
1664 public function allow_unenrol(stdClass $instance) {
1665 return false;
1666 }
1667
282b5cc7
PS
1668 /**
1669 * Does this plugin allow manual unenrolment of a specific user?
1670 * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1671 *
1672 * This is useful especially for synchronisation plugins that
1673 * do suspend instead of full unenrolment.
1674 *
1675 * @param stdClass $instance course enrol instance
1676 * @param stdClass $ue record from user_enrolments table, specifies user
1677 *
1678 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1679 */
1680 public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1681 return $this->allow_unenrol($instance);
1682 }
1683
df997f84
PS
1684 /**
1685 * Does this plugin allow manual changes in user_enrolments table?
1686 *
91b99e80 1687 * All plugins allowing this must implement 'enrol/xxx:manage' capability
df997f84
PS
1688 *
1689 * @param stdClass $instance course enrol instance
1690 * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1691 */
1692 public function allow_manage(stdClass $instance) {
1693 return false;
1694 }
1695
217d0397
PS
1696 /**
1697 * Does this plugin support some way to user to self enrol?
1698 *
1699 * @param stdClass $instance course enrol instance
1700 *
1701 * @return bool - true means show "Enrol me in this course" link in course UI
1702 */
1703 public function show_enrolme_link(stdClass $instance) {
1704 return false;
1705 }
1706
df997f84
PS
1707 /**
1708 * Attempt to automatically enrol current user in course without any interaction,
1709 * calling code has to make sure the plugin and instance are active.
1710 *
ed1d72ea
SH
1711 * This should return either a timestamp in the future or false.
1712 *
df997f84 1713 * @param stdClass $instance course enrol instance
df997f84
PS
1714 * @return bool|int false means not enrolled, integer means timeend
1715 */
1716 public function try_autoenrol(stdClass $instance) {
1717 global $USER;
1718
1719 return false;
1720 }
1721
1722 /**
1723 * Attempt to automatically gain temporary guest access to course,
1724 * calling code has to make sure the plugin and instance are active.
1725 *
ed1d72ea
SH
1726 * This should return either a timestamp in the future or false.
1727 *
df997f84 1728 * @param stdClass $instance course enrol instance
df997f84
PS
1729 * @return bool|int false means no guest access, integer means timeend
1730 */
1731 public function try_guestaccess(stdClass $instance) {
1732 global $USER;
1733
1734 return false;
1735 }
1736
1737 /**
1738 * Enrol user into course via enrol instance.
1739 *
1740 * @param stdClass $instance
1741 * @param int $userid
1742 * @param int $roleid optional role id
2a6dcb72
PS
1743 * @param int $timestart 0 means unknown
1744 * @param int $timeend 0 means forever
f2a9be5f 1745 * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
ef8a733a 1746 * @param bool $recovergrades restore grade history
df997f84
PS
1747 * @return void
1748 */
ef8a733a 1749 public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
df997f84
PS
1750 global $DB, $USER, $CFG; // CFG necessary!!!
1751
1752 if ($instance->courseid == SITEID) {
1753 throw new coding_exception('invalid attempt to enrol into frontpage course!');
1754 }
1755
1756 $name = $this->get_name();
1757 $courseid = $instance->courseid;
1758
1759 if ($instance->enrol !== $name) {
1760 throw new coding_exception('invalid enrol instance!');
1761 }
b0c6dc1c 1762 $context = context_course::instance($instance->courseid, MUST_EXIST);
ef8a733a
CF
1763 if (!isset($recovergrades)) {
1764 $recovergrades = $CFG->recovergradesdefault;
1765 }
df997f84
PS
1766
1767 $inserted = false;
358fb4dc 1768 $updated = false;
df997f84 1769 if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
d587f077 1770 //only update if timestart or timeend or status are different.
ae8c1f3d 1771 if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
bb78e249 1772 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
df997f84
PS
1773 }
1774 } else {
365a5941 1775 $ue = new stdClass();
df997f84 1776 $ue->enrolid = $instance->id;
ae8c1f3d 1777 $ue->status = is_null($status) ? ENROL_USER_ACTIVE : $status;
df997f84
PS
1778 $ue->userid = $userid;
1779 $ue->timestart = $timestart;
1780 $ue->timeend = $timeend;
6006774c 1781 $ue->modifierid = $USER->id;
2a6dcb72
PS
1782 $ue->timecreated = time();
1783 $ue->timemodified = $ue->timecreated;
df997f84
PS
1784 $ue->id = $DB->insert_record('user_enrolments', $ue);
1785
1786 $inserted = true;
1787 }
1788
df997f84 1789 if ($inserted) {
bb78e249
RT
1790 // Trigger event.
1791 $event = \core\event\user_enrolment_created::create(
1792 array(
1793 'objectid' => $ue->id,
1794 'courseid' => $courseid,
1795 'context' => $context,
1796 'relateduserid' => $ue->userid,
1797 'other' => array('enrol' => $name)
1798 )
1799 );
1800 $event->trigger();
5667e602
MG
1801 // Check if course contacts cache needs to be cleared.
1802 require_once($CFG->libdir . '/coursecatlib.php');
1803 coursecat::user_enrolment_changed($courseid, $ue->userid,
1804 $ue->status, $ue->timestart, $ue->timeend);
df997f84
PS
1805 }
1806
0c2701fd 1807 if ($roleid) {
bbfdff34 1808 // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
0c2701fd
PS
1809 if ($this->roles_protected()) {
1810 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1811 } else {
1812 role_assign($roleid, $userid, $context->id);
1813 }
1814 }
1815
ef8a733a
CF
1816 // Recover old grades if present.
1817 if ($recovergrades) {
1818 require_once("$CFG->libdir/gradelib.php");
1819 grade_recover_history_grades($userid, $courseid);
1820 }
1821
bbfdff34 1822 // reset current user enrolment caching
df997f84
PS
1823 if ($userid == $USER->id) {
1824 if (isset($USER->enrol['enrolled'][$courseid])) {
1825 unset($USER->enrol['enrolled'][$courseid]);
1826 }
1827 if (isset($USER->enrol['tempguest'][$courseid])) {
1828 unset($USER->enrol['tempguest'][$courseid]);
e922fe23 1829 remove_temp_course_roles($context);
df997f84
PS
1830 }
1831 }
1832 }
1833
1834 /**
1835 * Store user_enrolments changes and trigger event.
1836 *
358fb4dc
PS
1837 * @param stdClass $instance
1838 * @param int $userid
df997f84
PS
1839 * @param int $status
1840 * @param int $timestart
1841 * @param int $timeend
1842 * @return void
1843 */
1844 public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
5667e602 1845 global $DB, $USER, $CFG;
df997f84
PS
1846
1847 $name = $this->get_name();
1848
1849 if ($instance->enrol !== $name) {
1850 throw new coding_exception('invalid enrol instance!');
1851 }
1852
1853 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1854 // weird, user not enrolled
1855 return;
1856 }
1857
1858 $modified = false;
1859 if (isset($status) and $ue->status != $status) {
1860 $ue->status = $status;
1861 $modified = true;
1862 }
1863 if (isset($timestart) and $ue->timestart != $timestart) {
1864 $ue->timestart = $timestart;
1865 $modified = true;
1866 }
1867 if (isset($timeend) and $ue->timeend != $timeend) {
1868 $ue->timeend = $timeend;
1869 $modified = true;
1870 }
1871
1872 if (!$modified) {
1873 // no change
1874 return;
1875 }
1876
1877 $ue->modifierid = $USER->id;
ed63ffc7 1878 $ue->timemodified = time();
df997f84 1879 $DB->update_record('user_enrolments', $ue);
bbfdff34 1880 context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
df997f84 1881
eeb9ee8e 1882 // Invalidate core_access cache for get_suspended_userids.
03ecddb4 1883 cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
eeb9ee8e 1884
bb78e249
RT
1885 // Trigger event.
1886 $event = \core\event\user_enrolment_updated::create(
1887 array(
1888 'objectid' => $ue->id,
1889 'courseid' => $instance->courseid,
1890 'context' => context_course::instance($instance->courseid),
1891 'relateduserid' => $ue->userid,
1892 'other' => array('enrol' => $name)
1893 )
1894 );
1895 $event->trigger();
5667e602
MG
1896
1897 require_once($CFG->libdir . '/coursecatlib.php');
1898 coursecat::user_enrolment_changed($instance->courseid, $ue->userid,
1899 $ue->status, $ue->timestart, $ue->timeend);
df997f84
PS
1900 }
1901
1902 /**
1903 * Unenrol user from course,
1904 * the last unenrolment removes all remaining roles.
1905 *
1906 * @param stdClass $instance
1907 * @param int $userid
1908 * @return void
1909 */
1910 public function unenrol_user(stdClass $instance, $userid) {
1911 global $CFG, $USER, $DB;
7881024e 1912 require_once("$CFG->dirroot/group/lib.php");
df997f84
PS
1913
1914 $name = $this->get_name();
1915 $courseid = $instance->courseid;
1916
1917 if ($instance->enrol !== $name) {
1918 throw new coding_exception('invalid enrol instance!');
1919 }
b0c6dc1c 1920 $context = context_course::instance($instance->courseid, MUST_EXIST);
df997f84
PS
1921
1922 if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1923 // weird, user not enrolled
1924 return;
1925 }
1926
7881024e
PS
1927 // Remove all users groups linked to this enrolment instance.
1928 if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
1929 foreach ($gms as $gm) {
1930 groups_remove_member($gm->groupid, $gm->userid);
1931 }
1932 }
1933
df997f84
PS
1934 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1935 $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1936
1937 // add extra info and trigger event
1938 $ue->courseid = $courseid;
1939 $ue->enrol = $name;
1940
1941 $sql = "SELECT 'x'
1942 FROM {user_enrolments} ue
1943 JOIN {enrol} e ON (e.id = ue.enrolid)
7881024e 1944 WHERE ue.userid = :userid AND e.courseid = :courseid";
df997f84
PS
1945 if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1946 $ue->lastenrol = false;
7881024e 1947
df997f84
PS
1948 } else {
1949 // the big cleanup IS necessary!
df997f84
PS
1950 require_once("$CFG->libdir/gradelib.php");
1951
1952 // remove all remaining roles
1953 role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1954
1955 //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1956 groups_delete_group_members($courseid, $userid);
1957
1958 grade_user_unenrol($courseid, $userid);
1959
1960 $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1961
7d2800fb 1962 $ue->lastenrol = true; // means user not enrolled any more
df997f84 1963 }
bb78e249
RT
1964 // Trigger event.
1965 $event = \core\event\user_enrolment_deleted::create(
1966 array(
1967 'courseid' => $courseid,
1968 'context' => $context,
1969 'relateduserid' => $ue->userid,
4fb6600e 1970 'objectid' => $ue->id,
bb78e249
RT
1971 'other' => array(
1972 'userenrolment' => (array)$ue,
1973 'enrol' => $name
1974 )
1975 )
1976 );
1977 $event->trigger();
bbfdff34
PS
1978 // reset all enrol caches
1979 $context->mark_dirty();
1980
5667e602
MG
1981 // Check if courrse contacts cache needs to be cleared.
1982 require_once($CFG->libdir . '/coursecatlib.php');
1983 coursecat::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
1984
bbfdff34 1985 // reset current user enrolment caching
df997f84
PS
1986 if ($userid == $USER->id) {
1987 if (isset($USER->enrol['enrolled'][$courseid])) {
1988 unset($USER->enrol['enrolled'][$courseid]);
1989 }
1990 if (isset($USER->enrol['tempguest'][$courseid])) {
1991 unset($USER->enrol['tempguest'][$courseid]);
e922fe23 1992 remove_temp_course_roles($context);
df997f84
PS
1993 }
1994 }
1995 }
1996
1997 /**
1998 * Forces synchronisation of user enrolments.
1999 *
2000 * This is important especially for external enrol plugins,
2001 * this function is called for all enabled enrol plugins
2002 * right after every user login.
2003 *
2004 * @param object $user user record
2005 * @return void
2006 */
2007 public function sync_user_enrolments($user) {
2008 // override if necessary
2009 }
2010
60010fd6
DW
2011 /**
2012 * This returns false for backwards compatibility, but it is really recommended.
2013 *
2014 * @since Moodle 3.1
2015 * @return boolean
2016 */
2017 public function use_standard_editing_ui() {
2018 return false;
2019 }
2020
2021 /**
2022 * Return whether or not, given the current state, it is possible to add a new instance
2023 * of this enrolment plugin to the course.
2024 *
2025 * Default implementation is just for backwards compatibility.
2026 *
2027 * @param int $courseid
2028 * @return boolean
2029 */
2030 public function can_add_instance($courseid) {
2031 $link = $this->get_newinstance_link($courseid);
2032 return !empty($link);
2033 }
2034
51c736f0
DW
2035 /**
2036 * Return whether or not, given the current state, it is possible to edit an instance
2037 * of this enrolment plugin in the course. Used by the standard editing UI
2038 * to generate a link to the edit instance form if editing is allowed.
2039 *
2040 * @param stdClass $instance
2041 * @return boolean
2042 */
2043 public function can_edit_instance($instance) {
2044 $context = context_course::instance($instance->courseid);
2045
2046 return has_capability('enrol/' . $instance->enrol . ':config', $context);
2047 }
2048
df997f84
PS
2049 /**
2050 * Returns link to page which may be used to add new instance of enrolment plugin in course.
2051 * @param int $courseid
2052 * @return moodle_url page url
2053 */
e25f2466 2054 public function get_newinstance_link($courseid) {
df997f84
PS
2055 // override for most plugins, check if instance already exists in cases only one instance is supported
2056 return NULL;
2057 }
2058
2059 /**
ee9e079d 2060 * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
df997f84
PS
2061 */
2062 public function instance_deleteable($instance) {
648cddcb
AA
2063 throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2064 enrol_plugin::can_delete_instance() instead');
ee9e079d
DN
2065 }
2066
2067 /**
2068 * Is it possible to delete enrol instance via standard UI?
2069 *
b5a289c4 2070 * @param stdClass $instance
ee9e079d
DN
2071 * @return bool
2072 */
2073 public function can_delete_instance($instance) {
2074 return false;
df997f84
PS
2075 }
2076
b5a289c4
DNA
2077 /**
2078 * Is it possible to hide/show enrol instance via standard UI?
2079 *
2080 * @param stdClass $instance
2081 * @return bool
2082 */
2083 public function can_hide_show_instance($instance) {
2084 debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2085 return true;
2086 }
2087
df997f84
PS
2088 /**
2089 * Returns link to manual enrol UI if exists.
2090 * Does the access control tests automatically.
2091 *
2092 * @param object $instance
2093 * @return moodle_url
2094 */
2095 public function get_manual_enrol_link($instance) {
2096 return NULL;
2097 }
2098
2099 /**
2100 * Returns list of unenrol links for all enrol instances in course.
2101 *
217d0397 2102 * @param int $instance
bf423bb1 2103 * @return moodle_url or NULL if self unenrolment not supported
df997f84
PS
2104 */
2105 public function get_unenrolself_link($instance) {
2106 global $USER, $CFG, $DB;
2107
2108 $name = $this->get_name();
2109 if ($instance->enrol !== $name) {
2110 throw new coding_exception('invalid enrol instance!');
2111 }
2112
2113 if ($instance->courseid == SITEID) {
2114 return NULL;
2115 }
2116
2117 if (!enrol_is_enabled($name)) {
2118 return NULL;
2119 }
2120
2121 if ($instance->status != ENROL_INSTANCE_ENABLED) {
2122 return NULL;
2123 }
2124
df997f84
PS
2125 if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2126 return NULL;
2127 }
2128
b0c6dc1c 2129 $context = context_course::instance($instance->courseid, MUST_EXIST);
217d0397 2130
df997f84
PS
2131 if (!has_capability("enrol/$name:unenrolself", $context)) {
2132 return NULL;
2133 }
2134
2135 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2136 return NULL;
2137 }
2138
0e35ba6f 2139 return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
df997f84
PS
2140 }
2141
2142 /**
2143 * Adds enrol instance UI to course edit form
2144 *
2145 * @param object $instance enrol instance or null if does not exist yet
2146 * @param MoodleQuickForm $mform
2147 * @param object $data
2148 * @param object $context context of existing course or parent category if course does not exist
2149 * @return void
2150 */
2151 public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2152 // override - usually at least enable/disable switch, has to add own form header
2153 }
2154
60010fd6
DW
2155 /**
2156 * Adds form elements to add/edit instance form.
2157 *
2158 * @since Moodle 3.1
2159 * @param object $instance enrol instance or null if does not exist yet
2160 * @param MoodleQuickForm $mform
2161 * @param context $context
2162 * @return void
2163 */
2164 public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2165 // Do nothing by default.
2166 }
2167
2168 /**
2169 * Perform custom validation of the data used to edit the instance.
2170 *
2171 * @since Moodle 3.1
2172 * @param array $data array of ("fieldname"=>value) of submitted data
2173 * @param array $files array of uploaded files "element_name"=>tmp_file_path
2174 * @param object $instance The instance data loaded from the DB.
2175 * @param context $context The context of the instance we are editing
2176 * @return array of "element_name"=>"error_description" if there are errors,
2177 * or an empty array if everything is OK.
2178 */
2179 public function edit_instance_validation($data, $files, $instance, $context) {
2180 // No errors by default.
2181 debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2182 return array();
2183 }
2184
df997f84
PS
2185 /**
2186 * Validates course edit form data
2187 *
2188 * @param object $instance enrol instance or null if does not exist yet
2189 * @param array $data
2190 * @param object $context context of existing course or parent category if course does not exist
2191 * @return array errors array
2192 */
2193 public function course_edit_validation($instance, array $data, $context) {
2194 return array();
2195 }
2196
2197 /**
2198 * Called after updating/inserting course.
2199 *
2200 * @param bool $inserted true if course just inserted
2201 * @param object $course
2202 * @param object $data form data
2203 * @return void
2204 */
2205 public function course_updated($inserted, $course, $data) {
eafb7a72
PS
2206 if ($inserted) {
2207 if ($this->get_config('defaultenrol')) {
2208 $this->add_default_instance($course);
2209 }
2210 }
df997f84
PS
2211 }
2212
2213 /**
eafb7a72 2214 * Add new instance of enrol plugin.
df997f84
PS
2215 * @param object $course
2216 * @param array instance fields
0848a196 2217 * @return int id of new instance, null if can not be created
df997f84
PS
2218 */
2219 public function add_instance($course, array $fields = NULL) {
2220 global $DB;
2221
2222 if ($course->id == SITEID) {
2223 throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2224 }
2225
365a5941 2226 $instance = new stdClass();
df997f84
PS
2227 $instance->enrol = $this->get_name();
2228 $instance->status = ENROL_INSTANCE_ENABLED;
2229 $instance->courseid = $course->id;
2230 $instance->enrolstartdate = 0;
2231 $instance->enrolenddate = 0;
2232 $instance->timemodified = time();
2233 $instance->timecreated = $instance->timemodified;
2234 $instance->sortorder = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2235
2236 $fields = (array)$fields;
2237 unset($fields['enrol']);
2238 unset($fields['courseid']);
2239 unset($fields['sortorder']);
2240 foreach($fields as $field=>$value) {
2241 $instance->$field = $value;
2242 }
2243
080c7d47
MG
2244 $instance->id = $DB->insert_record('enrol', $instance);
2245
2246 \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2247
2248 return $instance->id;
df997f84
PS
2249 }
2250
60010fd6
DW
2251 /**
2252 * Update instance of enrol plugin.
2253 *
2254 * @since Moodle 3.1
2255 * @param stdClass $instance
2256 * @param stdClass $data modified instance fields
2257 * @return boolean
2258 */
2259 public function update_instance($instance, $data) {
2260 global $DB;
2261 $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2262 'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2263 'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2264 'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2265 'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2266 'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2267
2268 foreach ($properties as $key) {
2269 if (isset($data->$key)) {
2270 $instance->$key = $data->$key;
2271 }
2272 }
2273 $instance->timemodified = time();
2274
2275 $update = $DB->update_record('enrol', $instance);
2276 if ($update) {
2277 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2278 }
2279 return $update;
2280 }
2281
df997f84
PS
2282 /**
2283 * Add new instance of enrol plugin with default settings,
2284 * called when adding new instance manually or when adding new course.
2285 *
2286 * Not all plugins support this.
2287 *
2288 * @param object $course
2289 * @return int id of new instance or null if no default supported
2290 */
2291 public function add_default_instance($course) {
2292 return null;
2293 }
2294
af7177db
PS
2295 /**
2296 * Update instance status
2297 *
2298 * Override when plugin needs to do some action when enabled or disabled.
2299 *
2300 * @param stdClass $instance
2301 * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2302 * @return void
2303 */
2304 public function update_status($instance, $newstatus) {
2305 global $DB;
2306
2307 $instance->status = $newstatus;
2308 $DB->update_record('enrol', $instance);
2309
af7177db 2310 $context = context_course::instance($instance->courseid);
080c7d47
MG
2311 \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2312
2313 // Invalidate all enrol caches.
af7177db
PS
2314 $context->mark_dirty();
2315 }
2316
df997f84
PS
2317 /**
2318 * Delete course enrol plugin instance, unenrol all users.
2319 * @param object $instance
2320 * @return void
2321 */
2322 public function delete_instance($instance) {
2323 global $DB;
2324
2325 $name = $this->get_name();
2326 if ($instance->enrol !== $name) {
2327 throw new coding_exception('invalid enrol instance!');
2328 }
2329
2330 //first unenrol all users
2331 $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2332 foreach ($participants as $participant) {
2333 $this->unenrol_user($instance, $participant->userid);
2334 }
2335 $participants->close();
2336
2337 // now clean up all remainders that were not removed correctly
a1cedcc9
PS
2338 $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2339 $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
df997f84
PS
2340 $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2341
2342 // finally drop the enrol row
2343 $DB->delete_records('enrol', array('id'=>$instance->id));
af7177db 2344
af7177db 2345 $context = context_course::instance($instance->courseid);
080c7d47
MG
2346 \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2347
2348 // Invalidate all enrol caches.
af7177db 2349 $context->mark_dirty();
df997f84
PS
2350 }
2351
2352 /**
2353 * Creates course enrol form, checks if form submitted
2354 * and enrols user if necessary. It can also redirect.
2355 *
2356 * @param stdClass $instance
2357 * @return string html text, usually a form in a text box
2358 */
2359 public function enrol_page_hook(stdClass $instance) {
2360 return null;
2361 }
2362
85d1c53a
RT
2363 /**
2364 * Checks if user can self enrol.
2365 *
2366 * @param stdClass $instance enrolment instance
cc1b5015
RT
2367 * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2368 * used by navigation to improve performance.
2369 * @return bool|string true if successful, else error message or false
85d1c53a 2370 */
cc1b5015 2371 public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
85d1c53a
RT
2372 return false;
2373 }
2374
2375 /**
2376 * Return information for enrolment instance containing list of parameters required
2377 * for enrolment, name of enrolment plugin etc.
2378 *
2379 * @param stdClass $instance enrolment instance
2380 * @return array instance info.
2381 */
2382 public function get_enrol_info(stdClass $instance) {
2383 return null;
2384 }
2385
df997f84
PS
2386 /**
2387 * Adds navigation links into course admin block.
2388 *
2389 * By defaults looks for manage links only.
2390 *
2391 * @param navigation_node $instancesnode
bbfdff34 2392 * @param stdClass $instance
2d4b1f3e 2393 * @return void
df997f84
PS
2394 */
2395 public function add_course_navigation($instancesnode, stdClass $instance) {
f7589515
DW
2396 if ($this->use_standard_editing_ui()) {
2397 $context = context_course::instance($instance->courseid);
2398 $cap = 'enrol/' . $instance->enrol . ':config';
2399 if (has_capability($cap, $context)) {
2400 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2401 $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2402 $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2403 }
2404 }
df997f84
PS
2405 }
2406
2407 /**
2d4b1f3e
PS
2408 * Returns edit icons for the page with list of instances
2409 * @param stdClass $instance
2410 * @return array
df997f84 2411 */
2d4b1f3e 2412 public function get_action_icons(stdClass $instance) {
51c736f0
DW
2413 global $OUTPUT;
2414
2415 $icons = array();
2416 if ($this->use_standard_editing_ui()) {
2417 $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2418 $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2419 $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2420 array('class' => 'iconsmall')));
2421 }
2422 return $icons;
df997f84
PS
2423 }
2424
2425 /**
2426 * Reads version.php and determines if it is necessary
2427 * to execute the cron job now.
2428 * @return bool
2429 */
2430 public function is_cron_required() {
2431 global $CFG;
2432
2433 $name = $this->get_name();
2434 $versionfile = "$CFG->dirroot/enrol/$name/version.php";
365a5941 2435 $plugin = new stdClass();
df997f84
PS
2436 include($versionfile);
2437 if (empty($plugin->cron)) {
2438 return false;
2439 }
2440 $lastexecuted = $this->get_config('lastcron', 0);
2441 if ($lastexecuted + $plugin->cron < time()) {
2442 return true;
2443 } else {
2444 return false;
2445 }
2446 }
2447
2448 /**
2449 * Called for all enabled enrol plugins that returned true from is_cron_required().
2450 * @return void
2451 */
2452 public function cron() {
2453 }
2454
2455 /**
2456 * Called when user is about to be deleted
2457 * @param object $user
2458 * @return void
2459 */
2460 public function user_delete($user) {
2461 global $DB;
2462
2463 $sql = "SELECT e.*
2464 FROM {enrol} e
45fb2cf8
PS
2465 JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2466 WHERE e.enrol = :name AND ue.userid = :userid";
df997f84
PS
2467 $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2468
45fb2cf8 2469 $rs = $DB->get_recordset_sql($sql, $params);
df997f84
PS
2470 foreach($rs as $instance) {
2471 $this->unenrol_user($instance, $user->id);
2472 }
2473 $rs->close();
2474 }
df997f84 2475
b69ca6be
SH
2476 /**
2477 * Returns an enrol_user_button that takes the user to a page where they are able to
2478 * enrol users into the managers course through this plugin.
2479 *
2480 * Optional: If the plugin supports manual enrolments it can choose to override this
2481 * otherwise it shouldn't
2482 *
2483 * @param course_enrolment_manager $manager
2484 * @return enrol_user_button|false
2485 */
2486 public function get_manual_enrol_button(course_enrolment_manager $manager) {
2487 return false;
2488 }
291215f4
SH
2489
2490 /**
2491 * Gets an array of the user enrolment actions
2492 *
2493 * @param course_enrolment_manager $manager
2494 * @param stdClass $ue
2495 * @return array An array of user_enrolment_actions
2496 */
2497 public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
12a52d7c 2498 return array();
291215f4 2499 }
75ee207b
SH
2500
2501 /**
2502 * Returns true if the plugin has one or more bulk operations that can be performed on
2503 * user enrolments.
2504 *
f20edd52 2505 * @param course_enrolment_manager $manager
75ee207b
SH
2506 * @return bool
2507 */
f20edd52 2508 public function has_bulk_operations(course_enrolment_manager $manager) {
75ee207b
SH
2509 return false;
2510 }
2511
2512 /**
2513 * Return an array of enrol_bulk_enrolment_operation objects that define
2514 * the bulk actions that can be performed on user enrolments by the plugin.
2515 *
f20edd52 2516 * @param course_enrolment_manager $manager
75ee207b
SH
2517 * @return array
2518 */
f20edd52 2519 public function get_bulk_operations(course_enrolment_manager $manager) {
75ee207b
SH
2520 return array();
2521 }
7a7b8a1f 2522
d8f22c49
PS
2523 /**
2524 * Do any enrolments need expiration processing.
2525 *
2526 * Plugins that want to call this functionality must implement 'expiredaction' config setting.
2527 *
2528 * @param progress_trace $trace
2529 * @param int $courseid one course, empty mean all
2530 * @return bool true if any data processed, false if not
2531 */
2532 public function process_expirations(progress_trace $trace, $courseid = null) {
2533 global $DB;
2534
2535 $name = $this->get_name();
2536 if (!enrol_is_enabled($name)) {
2537 $trace->finished();
2538 return false;
2539 }
2540
2541 $processed = false;
2542 $params = array();
2543 $coursesql = "";
2544 if ($courseid) {
2545 $coursesql = "AND e.courseid = :courseid";
2546 }
2547
2548 // Deal with expired accounts.
2549 $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
2550
2551 if ($action == ENROL_EXT_REMOVED_UNENROL) {
2552 $instances = array();
2553 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2554 FROM {user_enrolments} ue
2555 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2556 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2557 WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
2558 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
2559
2560 $rs = $DB->get_recordset_sql($sql, $params);
2561 foreach ($rs as $ue) {
2562 if (!$processed) {
2563 $trace->output("Starting processing of enrol_$name expirations...");
2564 $processed = true;
2565 }
2566 if (empty($instances[$ue->enrolid])) {
2567 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2568 }
2569 $instance = $instances[$ue->enrolid];
2570 if (!$this->roles_protected()) {
2571 // Let's just guess what extra roles are supposed to be removed.
2572 if ($instance->roleid) {
2573 role_unassign($instance->roleid, $ue->userid, $ue->contextid);
2574 }
2575 }
2576 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
2577 $this->unenrol_user($instance, $ue->userid);
2578 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
2579 }
2580 $rs->close();
2581 unset($instances);
2582
2583 } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
2584 $instances = array();
2585 $sql = "SELECT ue.*, e.courseid, c.id AS contextid
2586 FROM {user_enrolments} ue
2587 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
2588 JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
2589 WHERE ue.timeend > 0 AND ue.timeend < :now
2590 AND ue.status = :useractive $coursesql";
2591 $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
2592 $rs = $DB->get_recordset_sql($sql, $params);
2593 foreach ($rs as $ue) {
2594 if (!$processed) {
2595 $trace->output("Starting processing of enrol_$name expirations...");
2596 $processed = true;
2597 }
2598 if (empty($instances[$ue->enrolid])) {
2599 $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
2600 }
2601 $instance = $instances[$ue->enrolid];
2602
2603 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
2604 if (!$this->roles_protected()) {
2605 // Let's just guess what roles should be removed.
2606 $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
2607 if ($count == 1) {
2608 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
2609
2610 } else if ($count > 1 and $instance->roleid) {
2611 role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
2612 }
2613 }
2614 // In any case remove all roles that belong to this instance and user.
2615 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
2616 // Final cleanup of subcontexts if there are no more course roles.
2617 if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
2618 role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
2619 }
2620 }
2621
2622 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
2623 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
2624 }
2625 $rs->close();
2626 unset($instances);
2627
2628 } else {
2629 // ENROL_EXT_REMOVED_KEEP means no changes.
2630 }
2631
2632 if ($processed) {
2633 $trace->output("...finished processing of enrol_$name expirations");
2634 } else {
2635 $trace->output("No expired enrol_$name enrolments detected");
2636 }
2637 $trace->finished();
2638
2639 return $processed;
2640 }
2641
8c04252c
PS
2642 /**
2643 * Send expiry notifications.
2644 *
2645 * Plugin that wants to have expiry notification MUST implement following:
2646 * - expirynotifyhour plugin setting,
2647 * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
2648 * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
2649 * expirymessageenrolledsubject and expirymessageenrolledbody),
2650 * - expiry_notification provider in db/messages.php,
2651 * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2652 * - something that calls this method, such as cron.
2653 *
5d549ffc 2654 * @param progress_trace $trace (accepts bool for backwards compatibility only)
8c04252c 2655 */
5d549ffc 2656 public function send_expiry_notifications($trace) {
8c04252c
PS
2657 global $DB, $CFG;
2658
d8f22c49
PS
2659 $name = $this->get_name();
2660 if (!enrol_is_enabled($name)) {
2661 $trace->finished();
2662 return;
2663 }
2664
8c04252c
PS
2665 // Unfortunately this may take a long time, it should not be interrupted,
2666 // otherwise users get duplicate notification.
2667
3ef7279f 2668 core_php_time_limit::raise();
8c04252c
PS
2669 raise_memory_limit(MEMORY_HUGE);
2670
8c04252c
PS
2671
2672 $expirynotifylast = $this->get_config('expirynotifylast', 0);
2673 $expirynotifyhour = $this->get_config('expirynotifyhour');
2674 if (is_null($expirynotifyhour)) {
2675 debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
5d549ffc 2676 $trace->finished();
8c04252c
PS
2677 return;
2678 }
2679
5d549ffc
PS
2680 if (!($trace instanceof progress_trace)) {
2681 $trace = $trace ? new text_progress_trace() : new null_progress_trace();
2682 debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
2683 }
2684
8c04252c
PS
2685 $timenow = time();
2686 $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
2687
2688 if ($expirynotifylast > $notifytime) {
5d549ffc
PS
2689 $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
2690 $trace->finished();
8c04252c 2691 return;
5d549ffc 2692
8c04252c 2693 } else if ($timenow < $notifytime) {
5d549ffc
PS
2694 $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
2695 $trace->finished();
8c04252c
PS
2696 return;
2697 }
2698
5d549ffc 2699 $trace->output('Processing '.$name.' enrolment expiration notifications...');
8c04252c
PS
2700
2701 // Notify users responsible for enrolment once every day.
2702 $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
2703 FROM {user_enrolments} ue
2704 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
2705 JOIN {course} c ON (c.id = e.courseid)
2706 JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
2707 WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
2708 ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
2709 $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
2710
2711 $rs = $DB->get_recordset_sql($sql, $params);
2712
2713 $lastenrollid = 0;
2714 $users = array();
2715
2716 foreach($rs as $ue) {
2717 if ($lastenrollid and $lastenrollid != $ue->enrolid) {
5d549ffc 2718 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
8c04252c
PS
2719 $users = array();
2720 }
2721 $lastenrollid = $ue->enrolid;
2722
2723 $enroller = $this->get_enroller($ue->enrolid);
2724 $context = context_course::instance($ue->courseid);
2725
2726 $user = $DB->get_record('user', array('id'=>$ue->userid));
2727
2728 $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
2729
2730 if (!$ue->notifyall) {
2731 continue;
2732 }
2733
2734 if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
2735 // Notify enrolled users only once at the start of the threshold.
5d549ffc 2736 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
8c04252c
PS
2737 continue;
2738 }
2739
5d549ffc 2740 $this->notify_expiry_enrolled($user, $ue, $trace);
8c04252c
PS
2741 }
2742 $rs->close();
2743
2744 if ($lastenrollid and $users) {
5d549ffc 2745 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
8c04252c
PS
2746 }
2747
5d549ffc
PS
2748 $trace->output('...notification processing finished.');
2749 $trace->finished();
2750
8c04252c
PS
2751 $this->set_config('expirynotifylast', $timenow);
2752 }
2753
2754 /**
2755 * Returns the user who is responsible for enrolments for given instance.
2756 *
2757 * Override if plugin knows anybody better than admin.
2758 *
2759 * @param int $instanceid enrolment instance id
2760 * @return stdClass user record
2761 */
2762 protected function get_enroller($instanceid) {
2763 return get_admin();
2764 }
2765
2766 /**
2767 * Notify user about incoming expiration of their enrolment,
2768 * it is called only if notification of enrolled users (aka students) is enabled in course.
2769 *
2770 * This is executed only once for each expiring enrolment right
2771 * at the start of the expiration threshold.
2772 *
2773 * @param stdClass $user
2774 * @param stdClass $ue
5d549ffc 2775 * @param progress_trace $trace
8c04252c 2776 */
5d549ffc 2777 protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
c484af5a 2778 global $CFG;
8c04252c
PS
2779
2780 $name = $this->get_name();
2781
c484af5a 2782 $oldforcelang = force_current_language($user->lang);
8c04252c
PS
2783
2784 $enroller = $this->get_enroller($ue->enrolid);
2785 $context = context_course::instance($ue->courseid);
2786
2787 $a = new stdClass();
2788 $a->course = format_string($ue->fullname, true, array('context'=>$context));
2789 $a->user = fullname($user, true);
2790 $a->timeend = userdate($ue->timeend, '', $user->timezone);
2791 $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
2792
2793 $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
2794 $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
2795
cc350fd9 2796 $message = new \core\message\message();
880fc15b 2797 $message->courseid = $ue->courseid;
8c04252c
PS
2798 $message->notification = 1;
2799 $message->component = 'enrol_'.$name;
2800 $message->name = 'expiry_notification';
2801 $message->userfrom = $enroller;
2802 $message->userto = $user;
2803 $message->subject = $subject;
2804 $message->fullmessage = $body;
2805 $message->fullmessageformat = FORMAT_MARKDOWN;
2806 $message->fullmessagehtml = markdown_to_html($body);
2807 $message->smallmessage = $subject;
2808 $message->contexturlname = $a->course;
2809 $message->contexturl = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
2810
2811 if (message_send($message)) {
5d549ffc 2812 $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
8c04252c 2813 } else {
5d549ffc 2814 $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
8c04252c
PS
2815 }
2816
c484af5a 2817 force_current_language($oldforcelang);
8c04252c
PS
2818 }
2819
2820 /**
2821 * Notify person responsible for enrolments that some user enrolments will be expired soon,
2822 * it is called only if notification of enrollers (aka teachers) is enabled in course.
2823 *
2824 * This is called repeatedly every day for each course if there are any pending expiration
2825 * in the expiration threshold.
2826 *
2827 * @param int $eid
2828 * @param array $users
5d549ffc 2829 * @param progress_trace $trace
8c04252c 2830 */
5d549ffc 2831 protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
c484af5a 2832 global $DB;
8c04252c
PS
2833
2834 $name = $this->get_name();
2835
2836 $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
2837 $context = context_course::instance($instance->courseid);
2838 $course = $DB->get_record('course', array('id'=>$instance->courseid));
2839
2840 $enroller = $this->get_enroller($instance->id);
2841 $admin = get_admin();
2842
c484af5a 2843 $oldforcelang = force_current_language($enroller->lang);
8c04252c
PS
2844
2845 foreach($users as $key=>$info) {
2846 $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
2847 }
2848
2849 $a = new stdClass();
2850 $a->course = format_string($course->fullname, true, array('context'=>$context));
2851 $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
2852 $a->users = implode("\n", $users);
2853 $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid));
2854
2855 $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
2856 $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
2857
cc350fd9 2858 $message = new \core\message\message();
880fc15b 2859 $message->courseid = $course->id;
8c04252c
PS
2860 $message->notification = 1;
2861 $message->component = 'enrol_'.$name;
2862 $message->name = 'expiry_notification';
2863 $message->userfrom = $admin;
2864 $message->userto = $enroller;
2865 $message->subject = $subject;
2866 $message->fullmessage = $body;
2867 $message->fullmessageformat = FORMAT_MARKDOWN;
2868 $message->fullmessagehtml = markdown_to_html($body);
2869 $message->smallmessage = $subject;
2870 $message->contexturlname = $a->course;
2871 $message->contexturl = $a->extendurl;
2872
2873 if (message_send($message)) {
5d549ffc 2874 $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
8c04252c 2875 } else {
5d549ffc 2876 $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
8c04252c
PS
2877 }
2878
c484af5a 2879 force_current_language($oldforcelang);
8c04252c
PS
2880 }
2881
f6199295
MP
2882 /**
2883 * Backup execution step hook to annotate custom fields.
2884 *
2885 * @param backup_enrolments_execution_step $step
2886 * @param stdClass $enrol
2887 */
2888 public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
2889 // Override as necessary to annotate custom fields in the enrol table.
2890 }
2891
7a7b8a1f
PS
2892 /**
2893 * Automatic enrol sync executed during restore.
2894 * Useful for automatic sync by course->idnumber or course category.
2895 * @param stdClass $course course record
2896 */
2897 public function restore_sync_course($course) {
2898 // Override if necessary.
2899 }
2900
2901 /**
2902 * Restore instance and map settings.
2903 *
2904 * @param restore_enrolments_structure_step $step
2905 * @param stdClass $data
2906 * @param stdClass $course
2907 * @param int $oldid
2908 */
2909 public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
2910 // Do not call this from overridden methods, restore and set new id there.
2911 $step->set_mapping('enrol', $oldid, 0);
2912 }
2913
2914 /**
2915 * Restore user enrolment.
2916 *
2917 * @param restore_enrolments_structure_step $step
2918 * @param stdClass $data
2919 * @param stdClass $instance
2920 * @param int $oldinstancestatus
2921 * @param int $userid
2922 */
2923 public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
2924 // Override as necessary if plugin supports restore of enrolments.
2925 }
2926
2927 /**
2928 * Restore role assignment.
2929 *
2930 * @param stdClass $instance
2931 * @param int $roleid
2932 * @param int $userid
2933 * @param int $contextid
2934 */
2935 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
2936 // No role assignment by default, override if necessary.
2937 }
7881024e
PS
2938
2939 /**
2940 * Restore user group membership.
2941 * @param stdClass $instance
2942 * @param int $groupid
2943 * @param int $userid
2944 */
2945 public function restore_group_member($instance, $groupid, $userid) {
2946 // Implement if you want to restore protected group memberships,
2947 // usually this is not necessary because plugins should be able to recreate the memberships automatically.
2948 }
60010fd6
DW
2949
2950 /**
2951 * Returns defaults for new instances.
2952 * @since Moodle 3.1
2953 * @return array
2954 */
2955 public function get_instance_defaults() {
2956 return array();
2957 }
2958
2959 /**
2960 * Validate a list of parameter names and types.
2961 * @since Moodle 3.1
2962 *
2963 * @param array $data array of ("fieldname"=>value) of submitted data
2964 * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
2965 * @return array of "element_name"=>"error_description" if there are errors,
2966 * or an empty array if everything is OK.
2967 */
2968 public function validate_param_types($data, $rules) {
2969 $errors = array();
2970 $invalidstr = get_string('invaliddata', 'error');
2971 foreach ($rules as $fieldname => $rule) {
2972 if (is_array($rule)) {
2973 if (!in_array($data[$fieldname], $rule)) {
2974 $errors[$fieldname] = $invalidstr;
2975 }
2976 } else {
2977 if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
2978 $errors[$fieldname] = $invalidstr;
2979 }
2980 }
2981 }
2982 return $errors;
2983 }
4b715423 2984}