Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
// This file is part of Moodle - http://moodle.org/
4
//
5
// Moodle is free software: you can redistribute it and/or modify
6
// it under the terms of the GNU General Public License as published by
7
// the Free Software Foundation, either version 3 of the License, or
8
// (at your option) any later version.
9
//
10
// Moodle is distributed in the hope that it will be useful,
11
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
// GNU General Public License for more details.
14
//
15
// You should have received a copy of the GNU General Public License
16
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17
 
18
/**
19
 * This library includes the basic parts of enrol api.
20
 * It is available on each page.
21
 *
22
 * @package    core
23
 * @subpackage enrol
24
 * @copyright  2010 Petr Skoda {@link http://skodak.org}
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
/** Course enrol instance enabled. (used in enrol->status) */
31
define('ENROL_INSTANCE_ENABLED', 0);
32
 
33
/** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
34
define('ENROL_INSTANCE_DISABLED', 1);
35
 
36
/** User is active participant (used in user_enrolments->status)*/
37
define('ENROL_USER_ACTIVE', 0);
38
 
39
/** User participation in course is suspended (used in user_enrolments->status) */
40
define('ENROL_USER_SUSPENDED', 1);
41
 
42
/** @deprecated - enrol caching was reworked, use ENROL_MAX_TIMESTAMP instead */
43
define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
44
 
45
/** The timestamp indicating forever */
46
define('ENROL_MAX_TIMESTAMP', 2147483647);
47
 
48
/** When user disappears from external source, the enrolment is completely removed */
49
define('ENROL_EXT_REMOVED_UNENROL', 0);
50
 
51
/** When user disappears from external source, the enrolment is kept as is - one way sync */
52
define('ENROL_EXT_REMOVED_KEEP', 1);
53
 
54
/** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */
55
define('ENROL_RESTORE_TYPE', 'enrolrestore');
56
 
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
 */
62
define('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
 * */
68
define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
69
 
70
/**
71
 * Do not send email.
72
 */
73
define('ENROL_DO_NOT_SEND_EMAIL', 0);
74
 
75
/**
76
 * Send email from course contact.
77
 */
78
define('ENROL_SEND_EMAIL_FROM_COURSE_CONTACT', 1);
79
 
80
/**
81
 * Send email from enrolment key holder.
82
 */
83
define('ENROL_SEND_EMAIL_FROM_KEY_HOLDER', 2);
84
 
85
/**
86
 * Send email from no reply address.
87
 */
88
define('ENROL_SEND_EMAIL_FROM_NOREPLY', 3);
89
 
90
/** Edit enrolment action. */
91
define('ENROL_ACTION_EDIT', 'editenrolment');
92
 
93
/** Unenrol action. */
94
define('ENROL_ACTION_UNENROL', 'unenrol');
95
 
96
/**
97
 * Returns instances of enrol plugins
98
 * @param bool $enabled return enabled only
99
 * @return array of enrol plugins name=>instance
100
 */
101
function 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
115
        $plugins = core_component::get_plugin_list('enrol');
116
        ksort($plugins);
117
    }
118
 
119
    foreach ($plugins as $plugin=>$location) {
120
        $class = "enrol_{$plugin}_plugin";
121
        if (!class_exists($class)) {
122
            if (!file_exists("$location/lib.php")) {
123
                continue;
124
            }
125
            include_once("$location/lib.php");
126
            if (!class_exists($class)) {
127
                continue;
128
            }
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
 */
142
function enrol_get_plugin($name) {
143
    global $CFG;
144
 
145
    $name = clean_param($name, PARAM_PLUGIN);
146
 
147
    if (empty($name)) {
148
        // ignore malformed or missing plugin names completely
149
        return null;
150
    }
151
 
152
    $location = "$CFG->dirroot/enrol/$name";
153
 
154
    $class = "enrol_{$name}_plugin";
155
    if (!class_exists($class)) {
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
        }
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
 */
174
function 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
 
183
    $enabled = explode(',', $CFG->enrol_plugins_enabled);
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
 */
205
function 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
 * This function may be very slow, use only once after log-in or login-as.
218
 *
219
 * @param stdClass $user User object.
220
 * @param bool $ignoreintervalcheck Force to ignore checking configured sync intervals.
221
 *
222
 * @return void
223
 */
224
function enrol_check_plugins($user, bool $ignoreintervalcheck = true) {
225
    global $CFG;
226
 
227
    if (empty($user->id) or isguestuser($user)) {
228
        // shortcut - there is no enrolment work for guests and not-logged-in users
229
        return;
230
    }
231
 
232
    // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
233
    // which proved it was actually not necessary.
234
 
235
    static $inprogress = array();  // To prevent this function being called more than once in an invocation
236
 
237
    if (!empty($inprogress[$user->id])) {
238
        return;
239
    }
240
 
241
    $syncinterval = isset($CFG->enrolments_sync_interval) ? (int)$CFG->enrolments_sync_interval : HOURSECS;
242
    $needintervalchecking = !$ignoreintervalcheck && !empty($syncinterval);
243
 
244
    if ($needintervalchecking) {
245
        $lastsync = get_user_preferences('last_time_enrolments_synced', 0, $user);
246
        if (time() - $lastsync < $syncinterval) {
247
            return;
248
        }
249
    }
250
 
251
    $inprogress[$user->id] = true;  // Set the flag
252
 
253
    $enabled = enrol_get_plugins(true);
254
 
255
    foreach($enabled as $enrol) {
256
        $enrol->sync_user_enrolments($user);
257
    }
258
 
259
    if ($needintervalchecking) {
260
        set_user_preference('last_time_enrolments_synced', time(), $user);
261
    }
262
 
263
    unset($inprogress[$user->id]);  // Unset the flag
264
}
265
 
266
/**
267
 * Do these two students share any course?
268
 *
269
 * The courses has to be visible and enrolments has to be active,
270
 * timestart and timeend restrictions are ignored.
271
 *
272
 * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
273
 * to true.
274
 *
275
 * @param stdClass|int $user1
276
 * @param stdClass|int $user2
277
 * @return bool
278
 */
279
function enrol_sharing_course($user1, $user2) {
280
    return enrol_get_shared_courses($user1, $user2, false, true);
281
}
282
 
283
/**
284
 * Returns any courses shared by the two users
285
 *
286
 * The courses has to be visible and enrolments has to be active,
287
 * timestart and timeend restrictions are ignored.
288
 *
289
 * @global moodle_database $DB
290
 * @param stdClass|int $user1
291
 * @param stdClass|int $user2
292
 * @param bool $preloadcontexts If set to true contexts for the returned courses
293
 *              will be preloaded.
294
 * @param bool $checkexistsonly If set to true then this function will return true
295
 *              if the users share any courses and false if not.
296
 * @return array|bool An array of courses that both users are enrolled in OR if
297
 *              $checkexistsonly set returns true if the users share any courses
298
 *              and false if not.
299
 */
300
function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
301
    global $DB, $CFG;
302
 
303
    $user1 = isset($user1->id) ? $user1->id : $user1;
304
    $user2 = isset($user2->id) ? $user2->id : $user2;
305
 
306
    if (empty($user1) or empty($user2)) {
307
        return false;
308
    }
309
 
310
    if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
311
        return false;
312
    }
313
 
314
    list($plugins1, $params1) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee1');
315
    list($plugins2, $params2) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee2');
316
    $params = array_merge($params1, $params2);
317
    $params['enabled1'] = ENROL_INSTANCE_ENABLED;
318
    $params['enabled2'] = ENROL_INSTANCE_ENABLED;
319
    $params['active1'] = ENROL_USER_ACTIVE;
320
    $params['active2'] = ENROL_USER_ACTIVE;
321
    $params['user1']   = $user1;
322
    $params['user2']   = $user2;
323
 
324
    $ctxselect = '';
325
    $ctxjoin = '';
326
    if ($preloadcontexts) {
327
        $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
328
        $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
329
        $params['contextlevel'] = CONTEXT_COURSE;
330
    }
331
 
332
    $sql = "SELECT c.* $ctxselect
333
              FROM {course} c
334
              JOIN (
335
                SELECT DISTINCT c.id
336
                  FROM {course} c
337
                  JOIN {enrol} e1 ON (c.id = e1.courseid AND e1.status = :enabled1 AND e1.enrol $plugins1)
338
                  JOIN {user_enrolments} ue1 ON (ue1.enrolid = e1.id AND ue1.status = :active1 AND ue1.userid = :user1)
339
                  JOIN {enrol} e2 ON (c.id = e2.courseid AND e2.status = :enabled2 AND e2.enrol $plugins2)
340
                  JOIN {user_enrolments} ue2 ON (ue2.enrolid = e2.id AND ue2.status = :active2 AND ue2.userid = :user2)
341
                 WHERE c.visible = 1
342
              ) ec ON ec.id = c.id
343
              $ctxjoin";
344
 
345
    if ($checkexistsonly) {
346
        return $DB->record_exists_sql($sql, $params);
347
    } else {
348
        $courses = $DB->get_records_sql($sql, $params);
349
        if ($preloadcontexts) {
350
            array_map('context_helper::preload_from_record', $courses);
351
        }
352
        return $courses;
353
    }
354
}
355
 
356
/**
357
 * This function adds necessary enrol plugins UI into the course edit form.
358
 *
359
 * @param MoodleQuickForm $mform
360
 * @param object $data course edit form data
361
 * @param object $context context of existing course or parent category if course does not exist
362
 * @return void
363
 */
364
function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
365
    $plugins = enrol_get_plugins(true);
366
    if (!empty($data->id)) {
367
        $instances = enrol_get_instances($data->id, false);
368
        foreach ($instances as $instance) {
369
            if (!isset($plugins[$instance->enrol])) {
370
                continue;
371
            }
372
            $plugin = $plugins[$instance->enrol];
373
            $plugin->course_edit_form($instance, $mform, $data, $context);
374
        }
375
    } else {
376
        foreach ($plugins as $plugin) {
377
            $plugin->course_edit_form(NULL, $mform, $data, $context);
378
        }
379
    }
380
}
381
 
382
/**
383
 * Validate course edit form data
384
 *
385
 * @param array $data raw form data
386
 * @param object $context context of existing course or parent category if course does not exist
387
 * @return array errors array
388
 */
389
function enrol_course_edit_validation(array $data, $context) {
390
    $errors = array();
391
    $plugins = enrol_get_plugins(true);
392
 
393
    if (!empty($data['id'])) {
394
        $instances = enrol_get_instances($data['id'], false);
395
        foreach ($instances as $instance) {
396
            if (!isset($plugins[$instance->enrol])) {
397
                continue;
398
            }
399
            $plugin = $plugins[$instance->enrol];
400
            $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
401
        }
402
    } else {
403
        foreach ($plugins as $plugin) {
404
            $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
405
        }
406
    }
407
 
408
    return $errors;
409
}
410
 
411
/**
412
 * Update enrol instances after course edit form submission
413
 * @param bool $inserted true means new course added, false course already existed
414
 * @param object $course
415
 * @param object $data form data
416
 * @return void
417
 */
418
function enrol_course_updated($inserted, $course, $data) {
419
    global $DB, $CFG;
420
 
421
    $plugins = enrol_get_plugins(true);
422
 
423
    foreach ($plugins as $plugin) {
424
        $plugin->course_updated($inserted, $course, $data);
425
    }
426
}
427
 
428
/**
429
 * Add navigation nodes
430
 * @param navigation_node $coursenode
431
 * @param object $course
432
 * @return void
433
 */
434
function enrol_add_course_navigation(navigation_node $coursenode, $course) {
435
    global $CFG;
436
 
437
    $coursecontext = context_course::instance($course->id);
438
 
439
    $instances = enrol_get_instances($course->id, true);
440
    $plugins   = enrol_get_plugins(true);
441
 
442
    // we do not want to break all course pages if there is some borked enrol plugin, right?
443
    foreach ($instances as $k=>$instance) {
444
        if (!isset($plugins[$instance->enrol])) {
445
            unset($instances[$k]);
446
        }
447
    }
448
 
449
    $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
450
 
451
    // List all participants - allows assigning roles, groups, etc.
452
    // Have this available even in the site context as the page is still accessible from the frontpage.
453
    if (has_capability('moodle/course:enrolreview', $coursecontext)) {
454
        $url = new moodle_url('/user/index.php', array('id' => $course->id));
455
        $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING,
456
            null, 'review', new pix_icon('i/enrolusers', ''));
457
    }
458
 
459
    if ($course->id != SITEID) {
460
        // manage enrol plugin instances
461
        if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
462
            $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
463
        } else {
464
            $url = NULL;
465
        }
466
        $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
467
 
468
        // each instance decides how to configure itself or how many other nav items are exposed
469
        foreach ($instances as $instance) {
470
            if (!isset($plugins[$instance->enrol])) {
471
                continue;
472
            }
473
            $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
474
        }
475
 
476
        if (!$url) {
477
            $instancesnode->trim_if_empty();
478
        }
479
 
480
        if (has_capability('moodle/course:renameroles', $coursecontext)) {
481
            $url = new moodle_url('/enrol/renameroles.php', array('id' => $course->id));
482
            $instancesnode->add(
483
                get_string('rolerenaming'),
484
                $url,
485
                navigation_node::TYPE_SETTING,
486
                null,
487
                'renameroles'
488
            );
489
        }
490
    }
491
 
492
    // Manage groups in this course or even frontpage
493
    if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
494
        $url = new moodle_url('/group/index.php', array('id'=>$course->id));
495
        $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
496
    }
497
 
498
    if (has_any_capability(
499
        [ 'moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:review'],
500
        $coursecontext
501
    )) {
502
        // Override roles
503
        if (has_capability('moodle/role:review', $coursecontext)) {
504
            $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
505
        } else {
506
            $url = NULL;
507
        }
508
        $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
509
 
510
        // Add assign or override roles if allowed
511
        if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
512
            if (has_capability('moodle/role:assign', $coursecontext)) {
513
                $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
514
                $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
515
            }
516
        }
517
        // Check role permissions
518
        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride', 'moodle/role:override'), $coursecontext)) {
519
            $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
520
            $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
521
        }
522
    }
523
 
524
     // Deal somehow with users that are not enrolled but still got a role somehow
525
    if ($course->id != SITEID) {
526
        //TODO, create some new UI for role assignments at course level
527
        if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
528
            $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
529
            $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
530
        }
531
    }
532
 
533
    // just in case nothing was actually added
534
    $usersnode->trim_if_empty();
535
 
536
    if ($course->id != SITEID) {
537
        if (isguestuser() or !isloggedin()) {
538
            // guest account can not be enrolled - no links for them
539
        } else if (is_enrolled($coursecontext)) {
540
            // unenrol link if possible
541
            foreach ($instances as $instance) {
542
                if (!isset($plugins[$instance->enrol])) {
543
                    continue;
544
                }
545
                $plugin = $plugins[$instance->enrol];
546
                if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
547
                    $coursenode->add(get_string('unenrolme', 'core_enrol'), $unenrollink,
548
                        navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
549
                    $coursenode->get('unenrolself')->set_force_into_more_menu(true);
550
                    break;
551
                    //TODO. deal with multiple unenrol links - not likely case, but still...
552
                }
553
            }
554
        } else {
555
            // enrol link if possible
556
            if (is_viewing($coursecontext)) {
557
                // better not show any enrol link, this is intended for managers and inspectors
558
            } else {
559
                foreach ($instances as $instance) {
560
                    if (!isset($plugins[$instance->enrol])) {
561
                        continue;
562
                    }
563
                    $plugin = $plugins[$instance->enrol];
564
                    if ($plugin->show_enrolme_link($instance)) {
565
                        $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
566
                        $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
567
                        $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
568
                        break;
569
                    }
570
                }
571
            }
572
        }
573
    }
574
}
575
 
576
/**
577
 * Returns list of courses current $USER is enrolled in and can access
578
 *
579
 * The $fields param is a list of field names to ADD so name just the fields you really need,
580
 * which will be added and uniq'd.
581
 *
582
 * If $allaccessible is true, this will additionally return courses that the current user is not
583
 * enrolled in, but can access because they are open to the user for other reasons (course view
584
 * permission, currently viewing course as a guest, or course allows guest access without
585
 * password).
586
 *
587
 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
588
 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
589
 * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
590
 * "ue" for the user_enrolments table.
591
 * @param int $limit max number of courses
592
 * @param array $courseids the list of course ids to filter by
593
 * @param bool $allaccessible Include courses user is not enrolled in, but can access
594
 * @param int $offset Offset the result set by this number
595
 * @param array $excludecourses IDs of hidden courses to exclude from search
596
 * @return array
597
 */
598
function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $courseids = [], $allaccessible = false,
599
    $offset = 0, $excludecourses = []) {
600
    global $DB, $USER, $CFG;
601
 
602
    // Allowed prefixes and field names.
603
    $allowedprefixesandfields = ['c' => array_keys($DB->get_columns('course')),
604
                                'ul' => array_keys($DB->get_columns('user_lastaccess')),
605
                                'ue' => array_keys($DB->get_columns('user_enrolments'))];
606
 
607
    // Re-Arrange the course sorting according to the admin settings.
608
    $sort = enrol_get_courses_sortingsql($sort);
609
 
610
    // Guest account does not have any enrolled courses.
611
    if (!$allaccessible && (isguestuser() or !isloggedin())) {
612
        return array();
613
    }
614
 
615
    $basefields = [
616
        'id', 'category', 'sortorder',
617
        'shortname', 'fullname', 'idnumber',
618
        'startdate', 'visible',
619
        'groupmode', 'groupmodeforce', 'cacherev',
620
        'showactivitydates', 'showcompletionconditions',
621
    ];
622
 
623
    if (empty($fields)) {
624
        $fields = $basefields;
625
    } else if (is_string($fields)) {
626
        // turn the fields from a string to an array
627
        $fields = explode(',', $fields);
628
        $fields = array_map('trim', $fields);
629
        $fields = array_unique(array_merge($basefields, $fields));
630
    } else if (is_array($fields)) {
631
        $fields = array_unique(array_merge($basefields, $fields));
632
    } else {
633
        throw new coding_exception('Invalid $fields parameter in enrol_get_my_courses()');
634
    }
635
    if (in_array('*', $fields)) {
636
        $fields = array('*');
637
    }
638
 
639
    $orderby = "";
640
    $sort    = trim($sort);
641
    $sorttimeaccess = false;
642
    if (!empty($sort)) {
643
        $rawsorts = explode(',', $sort);
644
        $sorts = array();
645
        foreach ($rawsorts as $rawsort) {
646
            $rawsort = trim($rawsort);
647
            // Make sure that there are no more white spaces in sortparams after explode.
648
            $sortparams = array_values(array_filter(explode(' ', $rawsort)));
649
            // If more than 2 values present then throw coding_exception.
650
            if (isset($sortparams[2])) {
651
                throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
652
            }
653
            // Check the sort ordering if present, at the beginning.
654
            if (isset($sortparams[1]) && (preg_match("/^(asc|desc)$/i", $sortparams[1]) === 0)) {
655
                throw new coding_exception('Invalid sort direction in $sort parameter in enrol_get_my_courses()');
656
            }
657
 
658
            $sortfield = $sortparams[0];
659
            $sortdirection = $sortparams[1] ?? 'asc';
660
            if (strpos($sortfield, '.') !== false) {
661
                $sortfieldparams = explode('.', $sortfield);
662
                // Check if more than one dots present in the prefix field.
663
                if (isset($sortfieldparams[2])) {
664
                    throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
665
                }
666
                list($prefix, $fieldname) = [$sortfieldparams[0], $sortfieldparams[1]];
667
                // Check if the field name matches with the allowed prefix.
668
                if (array_key_exists($prefix, $allowedprefixesandfields) &&
669
                    (in_array($fieldname, $allowedprefixesandfields[$prefix]))) {
670
                    if ($prefix === 'ul') {
671
                        $sorts[] = "COALESCE({$prefix}.{$fieldname}, 0) {$sortdirection}";
672
                        $sorttimeaccess = true;
673
                    } else {
674
                        // Check if the field name that matches with the prefix and just append to sorts.
675
                        $sorts[] = $rawsort;
676
                    }
677
                } else {
678
                    throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
679
                }
680
            } else {
681
                // Check if the field name matches with $allowedprefixesandfields.
682
                $found = false;
683
                foreach (array_keys($allowedprefixesandfields) as $prefix) {
684
                    if (in_array($sortfield, $allowedprefixesandfields[$prefix])) {
685
                        if ($prefix === 'ul') {
686
                            $sorts[] = "COALESCE({$prefix}.{$sortfield}, 0) {$sortdirection}";
687
                            $sorttimeaccess = true;
688
                        } else {
689
                            $sorts[] = "{$prefix}.{$sortfield} {$sortdirection}";
690
                        }
691
                        $found = true;
692
                        break;
693
                    }
694
                }
695
                if (!$found) {
696
                    // The param is not found in $allowedprefixesandfields.
697
                    throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
698
                }
699
            }
700
        }
701
        $sort = implode(',', $sorts);
702
        $orderby = "ORDER BY $sort";
703
    }
704
 
705
    $wheres = ['c.id <> ' . SITEID];
706
    $params = [];
707
 
708
    if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
709
        // list _only_ this course - anything else is asking for trouble...
710
        $wheres[] = "courseid = :loginas";
711
        $params['loginas'] = $USER->loginascontext->instanceid;
712
    }
713
 
714
    $coursefields = 'c.' .join(',c.', $fields);
715
    $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
716
    $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
717
    $params['contextlevel'] = CONTEXT_COURSE;
718
    $wheres = implode(" AND ", $wheres);
719
 
720
    $timeaccessselect = "";
721
    $timeaccessjoin = "";
722
 
723
    if (!empty($courseids)) {
724
        list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
725
        $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
726
        $params = array_merge($params, $courseidsparams);
727
    }
728
 
729
    if (!empty($excludecourses)) {
730
        list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($excludecourses, SQL_PARAMS_NAMED, 'param', false);
731
        $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
732
        $params = array_merge($params, $courseidsparams);
733
    }
734
 
735
    $courseidsql = "";
736
    // Logged-in, non-guest users get their enrolled courses.
737
    if (!isguestuser() && isloggedin()) {
738
        $courseidsql .= "
739
                SELECT DISTINCT e.courseid
740
                  FROM {enrol} e
741
                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
742
                 WHERE ue.status = :active AND e.status = :enabled AND ue.timestart <= :now1
743
                       AND (ue.timeend = 0 OR ue.timeend > :now2)";
744
        $params['userid1'] = $USER->id;
745
        $params['active'] = ENROL_USER_ACTIVE;
746
        $params['enabled'] = ENROL_INSTANCE_ENABLED;
747
        $params['now1'] = $params['now2'] = time();
748
 
749
        if ($sorttimeaccess) {
750
            $params['userid2'] = $USER->id;
751
            $timeaccessselect = ', ul.timeaccess as lastaccessed';
752
            $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
753
        }
754
    }
755
 
756
    // When including non-enrolled but accessible courses...
757
    if ($allaccessible) {
758
        if (is_siteadmin()) {
759
            // Site admins can access all courses.
760
            $courseidsql = "SELECT DISTINCT c2.id AS courseid FROM {course} c2";
761
        } else {
762
            // If we used the enrolment as well, then this will be UNIONed.
763
            if ($courseidsql) {
764
                $courseidsql .= " UNION ";
765
            }
766
 
767
            // Include courses with guest access and no password.
768
            $courseidsql .= "
769
                    SELECT DISTINCT e.courseid
770
                      FROM {enrol} e
771
                     WHERE e.enrol = 'guest' AND e.password = :emptypass AND e.status = :enabled2";
772
            $params['emptypass'] = '';
773
            $params['enabled2'] = ENROL_INSTANCE_ENABLED;
774
 
775
            // Include courses where the current user is currently using guest access (may include
776
            // those which require a password).
777
            $courseids = [];
778
            $accessdata = get_user_accessdata($USER->id);
779
            foreach ($accessdata['ra'] as $contextpath => $roles) {
780
                if (array_key_exists($CFG->guestroleid, $roles)) {
781
                    // Work out the course id from context path.
782
                    $context = context::instance_by_id(preg_replace('~^.*/~', '', $contextpath));
783
                    if ($context instanceof context_course) {
784
                        $courseids[$context->instanceid] = true;
785
                    }
786
                }
787
            }
788
 
789
            // Include courses where the current user has moodle/course:view capability.
790
            $courses = get_user_capability_course('moodle/course:view', null, false);
791
            if (!$courses) {
792
                $courses = [];
793
            }
794
            foreach ($courses as $course) {
795
                $courseids[$course->id] = true;
796
            }
797
 
798
            // If there are any in either category, list them individually.
799
            if ($courseids) {
800
                list ($allowedsql, $allowedparams) = $DB->get_in_or_equal(
801
                        array_keys($courseids), SQL_PARAMS_NAMED);
802
                $courseidsql .= "
803
                        UNION
804
                       SELECT DISTINCT c3.id AS courseid
805
                         FROM {course} c3
806
                        WHERE c3.id $allowedsql";
807
                $params = array_merge($params, $allowedparams);
808
            }
809
        }
810
    }
811
 
812
    // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
813
    // we have the subselect there.
814
    $sql = "SELECT $coursefields $ccselect $timeaccessselect
815
              FROM {course} c
816
              JOIN ($courseidsql) en ON (en.courseid = c.id)
817
           $timeaccessjoin
818
           $ccjoin
819
             WHERE $wheres
820
          $orderby";
821
 
822
    $courses = $DB->get_records_sql($sql, $params, $offset, $limit);
823
 
824
    // preload contexts and check visibility
825
    foreach ($courses as $id=>$course) {
826
        context_helper::preload_from_record($course);
827
        if (!$course->visible) {
828
            if (!$context = context_course::instance($id, IGNORE_MISSING)) {
829
                unset($courses[$id]);
830
                continue;
831
            }
832
            if (!has_capability('moodle/course:viewhiddencourses', $context)) {
833
                unset($courses[$id]);
834
                continue;
835
            }
836
        }
837
        $courses[$id] = $course;
838
    }
839
 
840
    //wow! Is that really all? :-D
841
 
842
    return $courses;
843
}
844
 
845
/**
846
 * Returns course enrolment information icons.
847
 *
848
 * @param object $course
849
 * @param array $instances enrol instances of this course, improves performance
850
 * @return array of pix_icon
851
 */
852
function enrol_get_course_info_icons($course, array $instances = NULL) {
853
    $icons = array();
854
    if (is_null($instances)) {
855
        $instances = enrol_get_instances($course->id, true);
856
    }
857
    $plugins = enrol_get_plugins(true);
858
    foreach ($plugins as $name => $plugin) {
859
        $pis = array();
860
        foreach ($instances as $instance) {
861
            if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
862
                debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
863
                continue;
864
            }
865
            if ($instance->enrol == $name) {
866
                $pis[$instance->id] = $instance;
867
            }
868
        }
869
        if ($pis) {
870
            $icons = array_merge($icons, $plugin->get_info_icons($pis));
871
        }
872
    }
873
    return $icons;
874
}
875
 
876
/**
877
 * Returns SQL ORDER arguments which reflect the admin settings to sort my courses.
878
 *
879
 * @param string|null $sort SQL ORDER arguments which were originally requested (optionally).
880
 * @return string SQL ORDER arguments.
881
 */
882
function enrol_get_courses_sortingsql($sort = null) {
883
    global $CFG;
884
 
885
    // Prepare the visible SQL fragment as empty.
886
    $visible = '';
887
    // Only create a visible SQL fragment if the caller didn't already pass a sort order which contains the visible field.
888
    if ($sort === null || strpos($sort, 'visible') === false) {
889
        // If the admin did not explicitly want to have shown and hidden courses sorted as one list, we will sort hidden
890
        // courses to the end of the course list.
891
        if (!isset($CFG->navsortmycourseshiddenlast) || $CFG->navsortmycourseshiddenlast == true) {
892
            $visible = 'visible DESC, ';
893
        }
894
    }
895
 
896
    // Only create a sortorder SQL fragment if the caller didn't already pass one.
897
    if ($sort === null) {
898
        // If the admin has configured a course sort order, we will use this.
899
        if (!empty($CFG->navsortmycoursessort)) {
900
            $sort = $CFG->navsortmycoursessort . ' ASC';
901
 
902
            // Otherwise we will fall back to the sortorder sorting.
903
        } else {
904
            $sort = 'sortorder ASC';
905
        }
906
    }
907
 
908
    return $visible . $sort;
909
}
910
 
911
/**
912
 * Returns course enrolment detailed information.
913
 *
914
 * @param object $course
915
 * @return array of html fragments - can be used to construct lists
916
 */
917
function enrol_get_course_description_texts($course) {
918
    $lines = array();
919
    $instances = enrol_get_instances($course->id, true);
920
    $plugins = enrol_get_plugins(true);
921
    foreach ($instances as $instance) {
922
        if (!isset($plugins[$instance->enrol])) {
923
            //weird
924
            continue;
925
        }
926
        $plugin = $plugins[$instance->enrol];
927
        $text = $plugin->get_description_text($instance);
928
        if ($text !== NULL) {
929
            $lines[] = $text;
930
        }
931
    }
932
    return $lines;
933
}
934
 
935
/**
936
 * Returns list of courses user is enrolled into.
937
 *
938
 * Note: Use {@link enrol_get_all_users_courses()} if you need the list without any capability checks.
939
 *
940
 * The $fields param is a list of field names to ADD so name just the fields you really need,
941
 * which will be added and uniq'd.
942
 *
943
 * @param int $userid User whose courses are returned, defaults to the current user.
944
 * @param bool $onlyactive Return only active enrolments in courses user may see.
945
 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
946
 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
947
 * @return array
948
 */
949
function enrol_get_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
950
    global $DB;
951
 
952
    $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
953
 
954
    // preload contexts and check visibility
955
    if ($onlyactive) {
956
        foreach ($courses as $id=>$course) {
957
            context_helper::preload_from_record($course);
958
            if (!$course->visible) {
959
                if (!$context = context_course::instance($id)) {
960
                    unset($courses[$id]);
961
                    continue;
962
                }
963
                if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
964
                    unset($courses[$id]);
965
                    continue;
966
                }
967
            }
968
        }
969
    }
970
 
971
    return $courses;
972
}
973
 
974
/**
975
 * Returns list of roles per users into course.
976
 *
977
 * @param int $courseid Course id.
978
 * @return array Array[$userid][$roleid] = role_assignment.
979
 */
980
function enrol_get_course_users_roles(int $courseid): array {
981
    global $DB;
982
 
983
    $context = context_course::instance($courseid);
984
 
985
    $roles = array();
986
 
987
    $records = $DB->get_recordset('role_assignments', array('contextid' => $context->id));
988
    foreach ($records as $record) {
989
        if (isset($roles[$record->userid]) === false) {
990
            $roles[$record->userid] = array();
991
        }
992
        $roles[$record->userid][$record->roleid] = $record;
993
    }
994
    $records->close();
995
 
996
    return $roles;
997
}
998
 
999
/**
1000
 * Can user access at least one enrolled course?
1001
 *
1002
 * Cheat if necessary, but find out as fast as possible!
1003
 *
1004
 * @param int|stdClass $user null means use current user
1005
 * @return bool
1006
 */
1007
function enrol_user_sees_own_courses($user = null) {
1008
    global $USER;
1009
 
1010
    if ($user === null) {
1011
        $user = $USER;
1012
    }
1013
    $userid = is_object($user) ? $user->id : $user;
1014
 
1015
    // Guest account does not have any courses
1016
    if (isguestuser($userid) or empty($userid)) {
1017
        return false;
1018
    }
1019
 
1020
    // Let's cheat here if this is the current user,
1021
    // if user accessed any course recently, then most probably
1022
    // we do not need to query the database at all.
1023
    if ($USER->id == $userid) {
1024
        if (!empty($USER->enrol['enrolled'])) {
1025
            foreach ($USER->enrol['enrolled'] as $until) {
1026
                if ($until > time()) {
1027
                    return true;
1028
                }
1029
            }
1030
        }
1031
    }
1032
 
1033
    // Now the slow way.
1034
    $courses = enrol_get_all_users_courses($userid, true);
1035
    foreach($courses as $course) {
1036
        if ($course->visible) {
1037
            return true;
1038
        }
1039
        context_helper::preload_from_record($course);
1040
        $context = context_course::instance($course->id);
1041
        if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
1042
            return true;
1043
        }
1044
    }
1045
 
1046
    return false;
1047
}
1048
 
1049
/**
1050
 * Returns list of courses user is enrolled into without performing any capability checks.
1051
 *
1052
 * The $fields param is a list of field names to ADD so name just the fields you really need,
1053
 * which will be added and uniq'd.
1054
 *
1055
 * @param int $userid User whose courses are returned, defaults to the current user.
1056
 * @param bool $onlyactive Return only active enrolments in courses user may see.
1057
 * @param string|array $fields Extra fields to be returned (array or comma-separated list).
1058
 * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
1059
 * @return array
1060
 */
1061
function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = null, $sort = null) {
1062
    global $DB;
1063
 
1064
    // Re-Arrange the course sorting according to the admin settings.
1065
    $sort = enrol_get_courses_sortingsql($sort);
1066
 
1067
    // Guest account does not have any courses
1068
    if (isguestuser($userid) or empty($userid)) {
1069
        return(array());
1070
    }
1071
 
1072
    $basefields = array('id', 'category', 'sortorder',
1073
            'shortname', 'fullname', 'idnumber',
1074
            'startdate', 'visible',
1075
            'defaultgroupingid',
1076
            'groupmode', 'groupmodeforce');
1077
 
1078
    if (empty($fields)) {
1079
        $fields = $basefields;
1080
    } else if (is_string($fields)) {
1081
        // turn the fields from a string to an array
1082
        $fields = explode(',', $fields);
1083
        $fields = array_map('trim', $fields);
1084
        $fields = array_unique(array_merge($basefields, $fields));
1085
    } else if (is_array($fields)) {
1086
        $fields = array_unique(array_merge($basefields, $fields));
1087
    } else {
1088
        throw new coding_exception('Invalid $fields parameter in enrol_get_all_users_courses()');
1089
    }
1090
    if (in_array('*', $fields)) {
1091
        $fields = array('*');
1092
    }
1093
 
1094
    $orderby = "";
1095
    $sort    = trim($sort);
1096
    if (!empty($sort)) {
1097
        $rawsorts = explode(',', $sort);
1098
        $sorts = array();
1099
        foreach ($rawsorts as $rawsort) {
1100
            $rawsort = trim($rawsort);
1101
            if (strpos($rawsort, 'c.') === 0) {
1102
                $rawsort = substr($rawsort, 2);
1103
            }
1104
            $sorts[] = trim($rawsort);
1105
        }
1106
        $sort = 'c.'.implode(',c.', $sorts);
1107
        $orderby = "ORDER BY $sort";
1108
    }
1109
 
1110
    $params = [];
1111
 
1112
    if ($onlyactive) {
1113
        $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
1114
        $params['now1']    = round(time(), -2); // improves db caching
1115
        $params['now2']    = $params['now1'];
1116
        $params['active']  = ENROL_USER_ACTIVE;
1117
        $params['enabled'] = ENROL_INSTANCE_ENABLED;
1118
    } else {
1119
        $subwhere = "";
1120
    }
1121
 
1122
    $coursefields = 'c.' .join(',c.', $fields);
1123
    $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1124
    $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1125
    $params['contextlevel'] = CONTEXT_COURSE;
1126
 
1127
    //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
1128
    $sql = "SELECT $coursefields $ccselect
1129
              FROM {course} c
1130
              JOIN (SELECT DISTINCT e.courseid
1131
                      FROM {enrol} e
1132
                      JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1133
                 $subwhere
1134
                   ) en ON (en.courseid = c.id)
1135
           $ccjoin
1136
             WHERE c.id <> " . SITEID . "
1137
          $orderby";
1138
    $params['userid']  = $userid;
1139
 
1140
    $courses = $DB->get_records_sql($sql, $params);
1141
 
1142
    return $courses;
1143
}
1144
 
1145
 
1146
 
1147
/**
1148
 * Called when user is about to be deleted.
1149
 * @param object $user
1150
 * @return void
1151
 */
1152
function enrol_user_delete($user) {
1153
    global $DB;
1154
 
1155
    $plugins = enrol_get_plugins(true);
1156
    foreach ($plugins as $plugin) {
1157
        $plugin->user_delete($user);
1158
    }
1159
 
1160
    // force cleanup of all broken enrolments
1161
    $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1162
}
1163
 
1164
/**
1165
 * Called when course is about to be deleted.
1166
 * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
1167
 * otherwise all enrolments in the course will be removed.
1168
 *
1169
 * @param stdClass $course
1170
 * @param int|null $userid
1171
 * @return void
1172
 */
1173
function enrol_course_delete($course, $userid = null) {
1174
    global $DB;
1175
 
1176
    $context = context_course::instance($course->id);
1177
    $instances = enrol_get_instances($course->id, false);
1178
    $plugins = enrol_get_plugins(true);
1179
 
1180
    if ($userid) {
1181
        // If the user id is present, include only course enrolment instances which allow manual unenrolment and
1182
        // the given user have a capability to perform unenrolment.
1183
        $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
1184
            $unenrolcap = "enrol/{$instance->enrol}:unenrol";
1185
            return $plugins[$instance->enrol]->allow_unenrol($instance) &&
1186
                has_capability($unenrolcap, $context, $userid);
1187
        });
1188
    }
1189
 
1190
    foreach ($instances as $instance) {
1191
        if (isset($plugins[$instance->enrol])) {
1192
            $plugins[$instance->enrol]->delete_instance($instance);
1193
        }
1194
        // low level delete in case plugin did not do it
1195
        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1196
        $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1197
        $DB->delete_records('enrol', array('id'=>$instance->id));
1198
    }
1199
}
1200
 
1201
/**
1202
 * Try to enrol user via default internal auth plugin.
1203
 *
1204
 * For now this is always using the manual enrol plugin...
1205
 *
1206
 * @param $courseid
1207
 * @param $userid
1208
 * @param $roleid
1209
 * @param $timestart
1210
 * @param $timeend
1211
 * @return bool success
1212
 */
1213
function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1214
    global $DB;
1215
 
1216
    //note: this is hardcoded to manual plugin for now
1217
 
1218
    if (!enrol_is_enabled('manual')) {
1219
        return false;
1220
    }
1221
 
1222
    if (!$enrol = enrol_get_plugin('manual')) {
1223
        return false;
1224
    }
1225
    if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1226
        return false;
1227
    }
1228
 
1229
    if ($roleid && !$DB->record_exists('role', ['id' => $roleid])) {
1230
        return false;
1231
    }
1232
 
1233
    $instance = reset($instances);
1234
    $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1235
 
1236
    return true;
1237
}
1238
 
1239
/**
1240
 * Is there a chance users might self enrol
1241
 * @param int $courseid
1242
 * @return bool
1243
 */
1244
function enrol_selfenrol_available($courseid) {
1245
    $result = false;
1246
 
1247
    $plugins = enrol_get_plugins(true);
1248
    $enrolinstances = enrol_get_instances($courseid, true);
1249
    foreach($enrolinstances as $instance) {
1250
        if (!isset($plugins[$instance->enrol])) {
1251
            continue;
1252
        }
1253
        if ($instance->enrol === 'guest') {
1254
            continue;
1255
        }
1256
        if ((isguestuser() || !isloggedin()) &&
1257
            ($plugins[$instance->enrol]->is_self_enrol_available($instance) === true)) {
1258
            $result = true;
1259
            break;
1260
        }
1261
        if ($plugins[$instance->enrol]->show_enrolme_link($instance) === true) {
1262
            $result = true;
1263
            break;
1264
        }
1265
    }
1266
 
1267
    return $result;
1268
}
1269
 
1270
/**
1271
 * This function returns the end of current active user enrolment.
1272
 *
1273
 * It deals correctly with multiple overlapping user enrolments.
1274
 *
1275
 * @param int $courseid
1276
 * @param int $userid
1277
 * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1278
 */
1279
function enrol_get_enrolment_end($courseid, $userid) {
1280
    global $DB;
1281
 
1282
    $sql = "SELECT ue.*
1283
              FROM {user_enrolments} ue
1284
              JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1285
              JOIN {user} u ON u.id = ue.userid
1286
             WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1287
    $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1288
 
1289
    if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1290
        return false;
1291
    }
1292
 
1293
    $changes = array();
1294
 
1295
    foreach ($enrolments as $ue) {
1296
        $start = (int)$ue->timestart;
1297
        $end = (int)$ue->timeend;
1298
        if ($end != 0 and $end < $start) {
1299
            debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1300
            continue;
1301
        }
1302
        if (isset($changes[$start])) {
1303
            $changes[$start] = $changes[$start] + 1;
1304
        } else {
1305
            $changes[$start] = 1;
1306
        }
1307
        if ($end === 0) {
1308
            // no end
1309
        } else if (isset($changes[$end])) {
1310
            $changes[$end] = $changes[$end] - 1;
1311
        } else {
1312
            $changes[$end] = -1;
1313
        }
1314
    }
1315
 
1316
    // let's sort then enrolment starts&ends and go through them chronologically,
1317
    // looking for current status and the next future end of enrolment
1318
    ksort($changes);
1319
 
1320
    $now = time();
1321
    $current = 0;
1322
    $present = null;
1323
 
1324
    foreach ($changes as $time => $change) {
1325
        if ($time > $now) {
1326
            if ($present === null) {
1327
                // we have just went past current time
1328
                $present = $current;
1329
                if ($present < 1) {
1330
                    // no enrolment active
1331
                    return false;
1332
                }
1333
            }
1334
            if ($present !== null) {
1335
                // we are already in the future - look for possible end
1336
                if ($current + $change < 1) {
1337
                    return $time;
1338
                }
1339
            }
1340
        }
1341
        $current += $change;
1342
    }
1343
 
1344
    if ($current > 0) {
1345
        return 0;
1346
    } else {
1347
        return false;
1348
    }
1349
}
1350
 
1351
/**
1352
 * Is current user accessing course via this enrolment method?
1353
 *
1354
 * This is intended for operations that are going to affect enrol instances.
1355
 *
1356
 * @param stdClass $instance enrol instance
1357
 * @return bool
1358
 */
1359
function enrol_accessing_via_instance(stdClass $instance) {
1360
    global $DB, $USER;
1361
 
1362
    if (empty($instance->id)) {
1363
        return false;
1364
    }
1365
 
1366
    if (is_siteadmin()) {
1367
        // Admins may go anywhere.
1368
        return false;
1369
    }
1370
 
1371
    return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1372
}
1373
 
1374
/**
1375
 * Returns true if user is enrolled (is participating) in course
1376
 * this is intended for students and teachers.
1377
 *
1378
 * Since 2.2 the result for active enrolments and current user are cached.
1379
 *
1380
 * @param context $context
1381
 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1382
 * @param string $withcapability extra capability name
1383
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1384
 * @return bool
1385
 */
1386
function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1387
    global $USER, $DB;
1388
 
1389
    // First find the course context.
1390
    $coursecontext = $context->get_course_context();
1391
 
1392
    // Make sure there is a real user specified.
1393
    if ($user === null) {
1394
        $userid = isset($USER->id) ? $USER->id : 0;
1395
    } else {
1396
        $userid = is_object($user) ? $user->id : $user;
1397
    }
1398
 
1399
    if (empty($userid)) {
1400
        // Not-logged-in!
1401
        return false;
1402
    } else if (isguestuser($userid)) {
1403
        // Guest account can not be enrolled anywhere.
1404
        return false;
1405
    }
1406
 
1407
    // Note everybody participates on frontpage, so for other contexts...
1408
    if ($coursecontext->instanceid != SITEID) {
1409
        // Try cached info first - the enrolled flag is set only when active enrolment present.
1410
        if ($USER->id == $userid) {
1411
            $coursecontext->reload_if_dirty();
1412
            if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1413
                if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1414
                    if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1415
                        return false;
1416
                    }
1417
                    return true;
1418
                }
1419
            }
1420
        }
1421
 
1422
        if ($onlyactive) {
1423
            // Look for active enrolments only.
1424
            $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1425
 
1426
            if ($until === false) {
1427
                return false;
1428
            }
1429
 
1430
            if ($USER->id == $userid) {
1431
                if ($until == 0) {
1432
                    $until = ENROL_MAX_TIMESTAMP;
1433
                }
1434
                $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1435
                if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1436
                    unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1437
                    remove_temp_course_roles($coursecontext);
1438
                }
1439
            }
1440
 
1441
        } else {
1442
            // Any enrolment is good for us here, even outdated, disabled or inactive.
1443
            $sql = "SELECT 'x'
1444
                      FROM {user_enrolments} ue
1445
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1446
                      JOIN {user} u ON u.id = ue.userid
1447
                     WHERE ue.userid = :userid AND u.deleted = 0";
1448
            $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1449
            if (!$DB->record_exists_sql($sql, $params)) {
1450
                return false;
1451
            }
1452
        }
1453
    }
1454
 
1455
    if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1456
        return false;
1457
    }
1458
 
1459
    return true;
1460
}
1461
 
1462
/**
1463
 * Returns an array of joins, wheres and params that will limit the group of
1464
 * users to only those enrolled and with given capability (if specified).
1465
 *
1466
 * Note this join will return duplicate rows for users who have been enrolled
1467
 * several times (e.g. as manual enrolment, and as self enrolment). You may
1468
 * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1469
 *
1470
 * In case is guaranteed some of the joins never match any rows, the resulting
1471
 * join_sql->cannotmatchanyrows will be true. This happens when the capability
1472
 * is prohibited.
1473
 *
1474
 * @param context $context
1475
 * @param string $prefix optional, a prefix to the user id column
1476
 * @param string|array $capability optional, may include a capability name, or array of names.
1477
 *      If an array is provided then this is the equivalent of a logical 'OR',
1478
 *      i.e. the user needs to have one of these capabilities.
1479
 * @param int|array|null $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1480
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1481
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1482
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1483
 * @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
1484
 */
1485
function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $groupids = 0,
1486
        $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1487
    $uid = $prefix . 'u.id';
1488
    $joins = array();
1489
    $wheres = array();
1490
    $cannotmatchanyrows = false;
1491
 
1492
    $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1493
    $joins[] = $enrolledjoin->joins;
1494
    $wheres[] = $enrolledjoin->wheres;
1495
    $params = $enrolledjoin->params;
1496
    $cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
1497
 
1498
    if (!empty($capability)) {
1499
        $capjoin = get_with_capability_join($context, $capability, $uid);
1500
        $joins[] = $capjoin->joins;
1501
        $wheres[] = $capjoin->wheres;
1502
        $params = array_merge($params, $capjoin->params);
1503
        $cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
1504
    }
1505
 
1506
    if ($groupids) {
1507
        $groupjoin = groups_get_members_join($groupids, $uid, $context);
1508
        $joins[] = $groupjoin->joins;
1509
        $params = array_merge($params, $groupjoin->params);
1510
        if (!empty($groupjoin->wheres)) {
1511
            $wheres[] = $groupjoin->wheres;
1512
        }
1513
        $cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
1514
    }
1515
 
1516
    $joins = implode("\n", $joins);
1517
    $wheres[] = "{$prefix}u.deleted = 0";
1518
    $wheres = implode(" AND ", $wheres);
1519
 
1520
    return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
1521
}
1522
 
1523
/**
1524
 * Returns array with sql code and parameters returning all ids
1525
 * of users enrolled into course.
1526
 *
1527
 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1528
 *
1529
 * @param context $context
1530
 * @param string $withcapability
1531
 * @param int|array|null $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1532
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1533
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1534
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1535
 * @return array list($sql, $params)
1536
 */
1537
function get_enrolled_sql(context $context, $withcapability = '', $groupids = 0, $onlyactive = false, $onlysuspended = false,
1538
                          $enrolid = 0) {
1539
 
1540
    // Use unique prefix just in case somebody makes some SQL magic with the result.
1541
    static $i = 0;
1542
    $i++;
1543
    $prefix = 'eu' . $i . '_';
1544
 
1545
    $capjoin = get_enrolled_with_capabilities_join(
1546
            $context, $prefix, $withcapability, $groupids, $onlyactive, $onlysuspended, $enrolid);
1547
 
1548
    $sql = "SELECT DISTINCT {$prefix}u.id
1549
              FROM {user} {$prefix}u
1550
            $capjoin->joins
1551
             WHERE $capjoin->wheres";
1552
 
1553
    return array($sql, $capjoin->params);
1554
}
1555
 
1556
/**
1557
 * Returns array with sql joins and parameters returning all ids
1558
 * of users enrolled into course.
1559
 *
1560
 * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1561
 *
1562
 * @throws coding_exception
1563
 *
1564
 * @param context $context
1565
 * @param string $useridcolumn User id column used the calling query, e.g. u.id
1566
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1567
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1568
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1569
 * @return \core\dml\sql_join Contains joins, wheres, params
1570
 */
1571
function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1572
    // Use unique prefix just in case somebody makes some SQL magic with the result.
1573
    static $i = 0;
1574
    $i++;
1575
    $prefix = 'ej' . $i . '_';
1576
 
1577
    // First find the course context.
1578
    $coursecontext = $context->get_course_context();
1579
 
1580
    $isfrontpage = ($coursecontext->instanceid == SITEID);
1581
 
1582
    if ($onlyactive && $onlysuspended) {
1583
        throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1584
    }
1585
    if ($isfrontpage && $onlysuspended) {
1586
        throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1587
    }
1588
 
1589
    $joins  = array();
1590
    $wheres = array();
1591
    $params = array();
1592
 
1593
    $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1594
 
1595
    // Note all users are "enrolled" on the frontpage, but for others...
1596
    if (!$isfrontpage) {
1597
        $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1598
        $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1599
 
1600
        $enrolconditions = array(
1601
            "{$prefix}e.id = {$prefix}ue.enrolid",
1602
            "{$prefix}e.courseid = :{$prefix}courseid",
1603
        );
1604
        if ($enrolid) {
1605
            $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1606
            $params[$prefix . 'enrolid'] = $enrolid;
1607
        }
1608
        $enrolconditionssql = implode(" AND ", $enrolconditions);
1609
        $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1610
 
1611
        $params[$prefix.'courseid'] = $coursecontext->instanceid;
1612
 
1613
        if (!$onlysuspended) {
1614
            $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1615
            $joins[] = $ejoin;
1616
            if ($onlyactive) {
1617
                $wheres[] = "$where1 AND $where2";
1618
            }
1619
        } else {
1620
            // Suspended only where there is enrolment but ALL are suspended.
1621
            // Consider multiple enrols where one is not suspended or plain role_assign.
1622
            $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1623
            $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1624
            $enrolconditions = array(
1625
                "{$prefix}e1.id = {$prefix}ue1.enrolid",
1626
                "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1627
            );
1628
            if ($enrolid) {
1629
                $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1630
                $params[$prefix . 'e1_enrolid'] = $enrolid;
1631
            }
1632
            $enrolconditionssql = implode(" AND ", $enrolconditions);
1633
            $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1634
            $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1635
            $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1636
        }
1637
 
1638
        if ($onlyactive || $onlysuspended) {
1639
            $now = round(time(), -2); // Rounding helps caching in DB.
1640
            $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1641
                    $prefix . 'active' => ENROL_USER_ACTIVE,
1642
                    $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1643
        }
1644
    }
1645
 
1646
    $joins = implode("\n", $joins);
1647
    $wheres = implode(" AND ", $wheres);
1648
 
1649
    return new \core\dml\sql_join($joins, $wheres, $params);
1650
}
1651
 
1652
/**
1653
 * Returns list of users enrolled into course.
1654
 *
1655
 * @param context $context
1656
 * @param string $withcapability
1657
 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1658
 * @param string $userfields requested user record fields
1659
 * @param string $orderby
1660
 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1661
 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1662
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1663
 * @return array of user records
1664
 */
1665
function get_enrolled_users(context $context, $withcapability = '', $groupids = 0, $userfields = 'u.*', $orderby = null,
1666
        $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1667
    global $DB;
1668
 
1669
    list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupids, $onlyactive);
1670
    $sql = "SELECT $userfields
1671
              FROM {user} u
1672
              JOIN ($esql) je ON je.id = u.id
1673
             WHERE u.deleted = 0";
1674
 
1675
    if ($orderby) {
1676
        $sql = "$sql ORDER BY $orderby";
1677
    } else {
1678
        list($sort, $sortparams) = users_order_by_sql('u');
1679
        $sql = "$sql ORDER BY $sort";
1680
        $params = array_merge($params, $sortparams);
1681
    }
1682
 
1683
    return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1684
}
1685
 
1686
/**
1687
 * Counts list of users enrolled into course (as per above function)
1688
 *
1689
 * @param context $context
1690
 * @param string $withcapability
1691
 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1692
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1693
 * @return int number of users enrolled into course
1694
 */
1695
function count_enrolled_users(context $context, $withcapability = '', $groupids = 0, $onlyactive = false) {
1696
    global $DB;
1697
 
1698
    $capjoin = get_enrolled_with_capabilities_join(
1699
            $context, '', $withcapability, $groupids, $onlyactive);
1700
 
1701
    $sql = "SELECT COUNT(DISTINCT u.id)
1702
              FROM {user} u
1703
            $capjoin->joins
1704
             WHERE $capjoin->wheres AND u.deleted = 0";
1705
 
1706
    return $DB->count_records_sql($sql, $capjoin->params);
1707
}
1708
 
1709
/**
1710
 * Send welcome email "from" options.
1711
 *
1712
 * @return array list of from options
1713
 */
1714
function enrol_send_welcome_email_options() {
1715
    return [
1716
        ENROL_DO_NOT_SEND_EMAIL                 => get_string('no'),
1717
        ENROL_SEND_EMAIL_FROM_COURSE_CONTACT    => get_string('sendfromcoursecontact', 'enrol'),
1718
        ENROL_SEND_EMAIL_FROM_KEY_HOLDER        => get_string('sendfromkeyholder', 'enrol'),
1719
        ENROL_SEND_EMAIL_FROM_NOREPLY           => get_string('sendfromnoreply', 'enrol')
1720
    ];
1721
}
1722
 
1723
/**
1724
 * Serve the user enrolment form as a fragment.
1725
 *
1726
 * @param array $args List of named arguments for the fragment loader.
1727
 * @return string
1728
 */
1729
function enrol_output_fragment_user_enrolment_form($args) {
1730
    global $CFG, $DB;
1731
 
1732
    $args = (object) $args;
1733
    $context = $args->context;
1734
    require_capability('moodle/course:enrolreview', $context);
1735
 
1736
    $ueid = $args->ueid;
1737
    $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1738
    $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1739
    $plugin = enrol_get_plugin($instance->enrol);
1740
    $customdata = [
1741
        'ue' => $userenrolment,
1742
        'modal' => true,
1743
        'enrolinstancename' => $plugin->get_instance_name($instance)
1744
    ];
1745
 
1746
    // Set the data if applicable.
1747
    $data = [];
1748
    if (isset($args->formdata)) {
1749
        $serialiseddata = json_decode($args->formdata);
1750
        parse_str($serialiseddata, $data);
1751
    }
1752
 
1753
    require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1754
    $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1755
 
1756
    if (!empty($data)) {
1757
        $mform->set_data($data);
1758
        $mform->is_validated();
1759
    }
1760
 
1761
    return $mform->render();
1762
}
1763
 
1764
/**
1765
 * Returns the course where a user enrolment belong to.
1766
 *
1767
 * @param int $ueid user_enrolments id
1768
 * @return stdClass
1769
 */
1770
function enrol_get_course_by_user_enrolment_id($ueid) {
1771
    global $DB;
1772
    $sql = "SELECT c.* FROM {user_enrolments} ue
1773
              JOIN {enrol} e ON e.id = ue.enrolid
1774
              JOIN {course} c ON c.id = e.courseid
1775
             WHERE ue.id = :ueid";
1776
    return $DB->get_record_sql($sql, array('ueid' => $ueid));
1777
}
1778
 
1779
/**
1780
 * Return all users enrolled in a course.
1781
 *
1782
 * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1783
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1784
 * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1785
 * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1786
 * @param array $usergroups Limit the results of users to the ones that belong to one of the submitted group ids.
1787
 * @return stdClass[]
1788
 */
1789
function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = [], $uefilter = [],
1790
                                $usergroups = []) {
1791
    global $DB;
1792
 
1793
    if (!$courseid && !$usersfilter && !$uefilter) {
1794
        throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1795
    }
1796
 
1797
    $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1798
             ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1799
             ue.timemodified AS uetimemodified, e.status AS estatus,
1800
             u.* FROM {user_enrolments} ue
1801
              JOIN {enrol} e ON e.id = ue.enrolid
1802
              JOIN {user} u ON ue.userid = u.id
1803
             WHERE ";
1804
    $params = array();
1805
 
1806
    if ($courseid) {
1807
        $conditions[] = "e.courseid = :courseid";
1808
        $params['courseid'] = $courseid;
1809
    }
1810
 
1811
    if ($onlyactive) {
1812
        $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1813
            "(ue.timeend = 0 OR ue.timeend > :now2)";
1814
        // Improves db caching.
1815
        $params['now1']    = round(time(), -2);
1816
        $params['now2']    = $params['now1'];
1817
        $params['active']  = ENROL_USER_ACTIVE;
1818
        $params['enabled'] = ENROL_INSTANCE_ENABLED;
1819
    }
1820
 
1821
    if ($usersfilter) {
1822
        list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1823
        $conditions[] = "ue.userid $usersql";
1824
        $params = $params + $userparams;
1825
    }
1826
 
1827
    if ($uefilter) {
1828
        list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1829
        $conditions[] = "ue.id $uesql";
1830
        $params = $params + $ueparams;
1831
    }
1832
 
1833
    // Only select enrolled users that belong to a specific group(s).
1834
    if (!empty($usergroups)) {
1835
        $usergroups = array_map(function ($item) { // Sanitize groupid to int to be save for sql.
1836
            return (int)$item;
1837
        }, $usergroups);
1838
        list($ugsql, $ugparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
1839
        $conditions[] = 'ue.userid IN (SELECT userid FROM {groups_members} WHERE groupid ' . $ugsql . ')';
1840
        $params = $params + $ugparams;
1841
    }
1842
 
1843
    return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1844
}
1845
 
1846
/**
1847
 * Get the list of options for the enrolment period dropdown
1848
 *
1849
 * @return array List of options for the enrolment period dropdown
1850
 */
1851
function enrol_get_period_list() {
1852
    $periodmenu = [];
1853
    $periodmenu[''] = get_string('unlimited');
1854
    for ($i = 1; $i <= 365; $i++) {
1855
        $seconds = $i * DAYSECS;
1856
        $periodmenu[$seconds] = get_string('numdays', '', $i);
1857
    }
1858
    return $periodmenu;
1859
}
1860
 
1861
/**
1862
 * Calculate duration base on start time and end time
1863
 *
1864
 * @param int $timestart Time start
1865
 * @param int $timeend Time end
1866
 * @return float|int Calculated duration
1867
 */
1868
function enrol_calculate_duration($timestart, $timeend) {
1869
    $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1870
    return $duration;
1871
}
1872
 
1873
/**
1874
 * Enrolment plugins abstract class.
1875
 *
1876
 * All enrol plugins should be based on this class,
1877
 * this is also the main source of documentation.
1878
 *
1879
 * @copyright  2010 Petr Skoda {@link http://skodak.org}
1880
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1881
 */
1882
abstract class enrol_plugin {
1883
    protected $config = null;
1884
 
1885
    /**
1886
     * Returns name of this enrol plugin
1887
     * @return string
1888
     */
1889
    public function get_name() {
1890
        // second word in class is always enrol name, sorry, no fancy plugin names with _
1891
        $words = explode('_', get_class($this));
1892
        return $words[1];
1893
    }
1894
 
1895
    /**
1896
     * Returns localised name of enrol instance
1897
     *
1898
     * @param object $instance (null is accepted too)
1899
     * @return string
1900
     */
1901
    public function get_instance_name($instance) {
1902
        if (empty($instance->name)) {
1903
            $enrol = $this->get_name();
1904
            return get_string('pluginname', 'enrol_'.$enrol);
1905
        } else {
1906
            $context = context_course::instance($instance->courseid);
1907
            return format_string($instance->name, true, array('context'=>$context));
1908
        }
1909
    }
1910
 
1911
    /**
1912
     * Returns optional enrolment information icons.
1913
     *
1914
     * This is used in course list for quick overview of enrolment options.
1915
     *
1916
     * We are not using single instance parameter because sometimes
1917
     * we might want to prevent icon repetition when multiple instances
1918
     * of one type exist. One instance may also produce several icons.
1919
     *
1920
     * @param array $instances all enrol instances of this type in one course
1921
     * @return array of pix_icon
1922
     */
1923
    public function get_info_icons(array $instances) {
1924
        return array();
1925
    }
1926
 
1927
    /**
1928
     * Returns optional enrolment instance description text.
1929
     *
1930
     * This is used in detailed course information.
1931
     *
1932
     *
1933
     * @param object $instance
1934
     * @return string short html text
1935
     */
1936
    public function get_description_text($instance) {
1937
        return null;
1938
    }
1939
 
1940
    /**
1941
     * Makes sure config is loaded and cached.
1942
     * @return void
1943
     */
1944
    protected function load_config() {
1945
        if (!isset($this->config)) {
1946
            $name = $this->get_name();
1947
            $this->config = get_config("enrol_$name");
1948
        }
1949
    }
1950
 
1951
    /**
1952
     * Returns plugin config value
1953
     * @param  string $name
1954
     * @param  string $default value if config does not exist yet
1955
     * @return string value or default
1956
     */
1957
    public function get_config($name, $default = NULL) {
1958
        $this->load_config();
1959
        return isset($this->config->$name) ? $this->config->$name : $default;
1960
    }
1961
 
1962
    /**
1963
     * Sets plugin config value
1964
     * @param  string $name name of config
1965
     * @param  string $value string config value, null means delete
1966
     */
1967
    public function set_config($name, $value) {
1968
        $pluginname = $this->get_name();
1969
        $this->load_config();
1970
        if ($value === NULL) {
1971
            unset($this->config->$name);
1972
        } else {
1973
            $this->config->$name = $value;
1974
        }
1975
        set_config($name, $value, "enrol_$pluginname");
1976
    }
1977
 
1978
    /**
1979
     * Does this plugin assign protected roles are can they be manually removed?
1980
     * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1981
     */
1982
    public function roles_protected() {
1983
        return true;
1984
    }
1985
 
1986
    /**
1987
     * Does this plugin allow manual enrolments?
1988
     *
1989
     * @param stdClass $instance course enrol instance
1990
     * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1991
     *
1992
     * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1993
     */
1994
    public function allow_enrol(stdClass $instance) {
1995
        return false;
1996
    }
1997
 
1998
    /**
1999
     * Does this plugin allow manual unenrolment of all users?
2000
     * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2001
     *
2002
     * @param stdClass $instance course enrol instance
2003
     * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
2004
     */
2005
    public function allow_unenrol(stdClass $instance) {
2006
        return false;
2007
    }
2008
 
2009
    /**
2010
     * Does this plugin allow manual unenrolment of a specific user?
2011
     * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2012
     *
2013
     * This is useful especially for synchronisation plugins that
2014
     * do suspend instead of full unenrolment.
2015
     *
2016
     * @param stdClass $instance course enrol instance
2017
     * @param stdClass $ue record from user_enrolments table, specifies user
2018
     *
2019
     * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
2020
     */
2021
    public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
2022
        return $this->allow_unenrol($instance);
2023
    }
2024
 
2025
    /**
2026
     * Does this plugin allow manual changes in user_enrolments table?
2027
     *
2028
     * All plugins allowing this must implement 'enrol/xxx:manage' capability
2029
     *
2030
     * @param stdClass $instance course enrol instance
2031
     * @return bool - true means it is possible to change enrol period and status in user_enrolments table
2032
     */
2033
    public function allow_manage(stdClass $instance) {
2034
        return false;
2035
    }
2036
 
2037
    /**
2038
     * Does this plugin support some way to user to self enrol?
2039
     *
2040
     * @param stdClass $instance course enrol instance
2041
     *
2042
     * @return bool - true means show "Enrol me in this course" link in course UI
2043
     */
2044
    public function show_enrolme_link(stdClass $instance) {
2045
        return false;
2046
    }
2047
 
2048
    /**
2049
     * Does this plugin support some way to self enrol?
2050
     * This function doesn't check user capabilities. Use can_self_enrol to check capabilities.
2051
     *
2052
     * @param stdClass $instance enrolment instance
2053
     * @return bool - true means "Enrol me in this course" link could be available.
2054
     */
2055
    public function is_self_enrol_available(stdClass $instance) {
2056
        return false;
2057
    }
2058
 
2059
    /**
2060
     * Attempt to automatically enrol current user in course without any interaction,
2061
     * calling code has to make sure the plugin and instance are active.
2062
     *
2063
     * This should return either a timestamp in the future or false.
2064
     *
2065
     * @param stdClass $instance course enrol instance
2066
     * @return bool|int false means not enrolled, integer means timeend
2067
     */
2068
    public function try_autoenrol(stdClass $instance) {
2069
        global $USER;
2070
 
2071
        return false;
2072
    }
2073
 
2074
    /**
2075
     * Attempt to automatically gain temporary guest access to course,
2076
     * calling code has to make sure the plugin and instance are active.
2077
     *
2078
     * This should return either a timestamp in the future or false.
2079
     *
2080
     * @param stdClass $instance course enrol instance
2081
     * @return bool|int false means no guest access, integer means timeend
2082
     */
2083
    public function try_guestaccess(stdClass $instance) {
2084
        global $USER;
2085
 
2086
        return false;
2087
    }
2088
 
2089
    /**
2090
     * Enrol user into course via enrol instance.
2091
     *
2092
     * @param stdClass $instance
2093
     * @param int $userid
2094
     * @param int $roleid optional role id
2095
     * @param int $timestart 0 means unknown
2096
     * @param int $timeend 0 means forever
2097
     * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
2098
     * @param bool $recovergrades restore grade history
2099
     * @return void
2100
     */
2101
    public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
2102
        global $DB, $USER, $CFG; // CFG necessary!!!
2103
 
2104
        if ($instance->courseid == SITEID) {
2105
            throw new coding_exception('invalid attempt to enrol into frontpage course!');
2106
        }
2107
 
2108
        $name = $this->get_name();
2109
        $courseid = $instance->courseid;
2110
 
2111
        if ($instance->enrol !== $name) {
2112
            throw new coding_exception('invalid enrol instance!');
2113
        }
2114
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2115
        if (!isset($recovergrades)) {
2116
            $recovergrades = $CFG->recovergradesdefault;
2117
        }
2118
 
2119
        $inserted = false;
2120
        $updated  = false;
2121
        if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2122
            //only update if timestart or timeend or status are different.
2123
            if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
2124
                $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
2125
            }
2126
        } else {
2127
            $ue = new stdClass();
2128
            $ue->enrolid      = $instance->id;
2129
            $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
2130
            $ue->userid       = $userid;
2131
            $ue->timestart    = $timestart;
2132
            $ue->timeend      = $timeend;
2133
            $ue->modifierid   = $USER->id;
2134
            $ue->timecreated  = time();
2135
            $ue->timemodified = $ue->timecreated;
2136
            $ue->id = $DB->insert_record('user_enrolments', $ue);
2137
 
2138
            $inserted = true;
2139
        }
2140
 
2141
        if ($inserted) {
2142
            // Trigger event.
2143
            $event = \core\event\user_enrolment_created::create(
2144
                    array(
2145
                        'objectid' => $ue->id,
2146
                        'courseid' => $courseid,
2147
                        'context' => $context,
2148
                        'relateduserid' => $ue->userid,
2149
                        'other' => array('enrol' => $name)
2150
                        )
2151
                    );
2152
            $event->trigger();
2153
            // Check if course contacts cache needs to be cleared.
2154
            core_course_category::user_enrolment_changed($courseid, $ue->userid,
2155
                    $ue->status, $ue->timestart, $ue->timeend);
2156
        }
2157
 
2158
        // Dispatch the hook for post enrol user actions.
2159
        $hook = new \core_enrol\hook\after_user_enrolled(
2160
            enrolinstance: $instance,
2161
            userenrolmentinstance: $ue,
2162
        );
2163
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2164
 
2165
        if ($roleid) {
2166
            // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2167
            if ($this->roles_protected()) {
2168
                role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2169
            } else {
2170
                role_assign($roleid, $userid, $context->id);
2171
            }
2172
        }
2173
 
2174
        // Recover old grades if present.
2175
        if ($recovergrades) {
2176
            require_once("$CFG->libdir/gradelib.php");
2177
            grade_recover_history_grades($userid, $courseid);
2178
        }
2179
 
2180
        // reset current user enrolment caching
2181
        if ($userid == $USER->id) {
2182
            if (isset($USER->enrol['enrolled'][$courseid])) {
2183
                unset($USER->enrol['enrolled'][$courseid]);
2184
            }
2185
            if (isset($USER->enrol['tempguest'][$courseid])) {
2186
                unset($USER->enrol['tempguest'][$courseid]);
2187
                remove_temp_course_roles($context);
2188
            }
2189
        }
2190
    }
2191
 
2192
    /**
2193
     * Store user_enrolments changes and trigger event.
2194
     *
2195
     * @param stdClass $instance
2196
     * @param int $userid
2197
     * @param int $status
2198
     * @param int $timestart
2199
     * @param int $timeend
2200
     * @return void
2201
     */
2202
    public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2203
        global $DB, $USER, $CFG;
2204
 
2205
        $name = $this->get_name();
2206
 
2207
        if ($instance->enrol !== $name) {
2208
            throw new coding_exception('invalid enrol instance!');
2209
        }
2210
 
2211
        if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2212
            // weird, user not enrolled
2213
            return;
2214
        }
2215
 
2216
        $modified = false;
2217
        $statusmodified = false;
2218
        $timeendmodified = false;
2219
        if (isset($status) and $ue->status != $status) {
2220
            $ue->status = $status;
2221
            $modified = true;
2222
            $statusmodified = true;
2223
        }
2224
        if (isset($timestart) and $ue->timestart != $timestart) {
2225
            $ue->timestart = $timestart;
2226
            $modified = true;
2227
        }
2228
        if (isset($timeend) and $ue->timeend != $timeend) {
2229
            $ue->timeend = $timeend;
2230
            $modified = true;
2231
            $timeendmodified = true;
2232
        }
2233
 
2234
        if (!$modified) {
2235
            // no change
2236
            return;
2237
        }
2238
 
2239
        // Dispatch the hook for pre user enrolment update actions.
2240
        $hook = new \core_enrol\hook\before_user_enrolment_updated(
2241
            enrolinstance: $instance,
2242
            userenrolmentinstance: $ue,
2243
            statusmodified: $statusmodified,
2244
            timeendmodified: $timeendmodified,
2245
        );
2246
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2247
 
2248
        $ue->modifierid = $USER->id;
2249
        $ue->timemodified = time();
2250
        $DB->update_record('user_enrolments', $ue);
2251
 
2252
        // User enrolments have changed, so mark user as dirty.
2253
        mark_user_dirty($userid);
2254
 
2255
        // Invalidate core_access cache for get_suspended_userids.
2256
        cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2257
 
2258
        // Trigger event.
2259
        $event = \core\event\user_enrolment_updated::create(
2260
                array(
2261
                    'objectid' => $ue->id,
2262
                    'courseid' => $instance->courseid,
2263
                    'context' => context_course::instance($instance->courseid),
2264
                    'relateduserid' => $ue->userid,
2265
                    'other' => array('enrol' => $name)
2266
                    )
2267
                );
2268
        $event->trigger();
2269
 
2270
        core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2271
                $ue->status, $ue->timestart, $ue->timeend);
2272
    }
2273
 
2274
    /**
2275
     * Unenrol user from course,
2276
     * the last unenrolment removes all remaining roles.
2277
     *
2278
     * @param stdClass $instance
2279
     * @param int $userid
2280
     * @return void
2281
     */
2282
    public function unenrol_user(stdClass $instance, $userid) {
2283
        global $CFG, $USER, $DB;
2284
        require_once("$CFG->dirroot/group/lib.php");
2285
 
2286
        $name = $this->get_name();
2287
        $courseid = $instance->courseid;
2288
 
2289
        if ($instance->enrol !== $name) {
2290
            throw new coding_exception('invalid enrol instance!');
2291
        }
2292
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2293
 
2294
        if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2295
            // weird, user not enrolled
2296
            return;
2297
        }
2298
 
2299
        // Dispatch the hook for pre user unenrolment actions.
2300
        $hook = new \core_enrol\hook\before_user_enrolment_removed(
2301
            enrolinstance: $instance,
2302
            userenrolmentinstance: $ue,
2303
        );
2304
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2305
 
2306
        // Remove all users groups linked to this enrolment instance.
2307
        if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2308
            foreach ($gms as $gm) {
2309
                groups_remove_member($gm->groupid, $gm->userid);
2310
            }
2311
        }
2312
 
2313
        role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2314
        $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2315
 
2316
        // add extra info and trigger event
2317
        $ue->courseid  = $courseid;
2318
        $ue->enrol     = $name;
2319
 
2320
        $sql = "SELECT 'x'
2321
                  FROM {user_enrolments} ue
2322
                  JOIN {enrol} e ON (e.id = ue.enrolid)
2323
                 WHERE ue.userid = :userid AND e.courseid = :courseid";
2324
        if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2325
            $ue->lastenrol = false;
2326
 
2327
        } else {
2328
            // the big cleanup IS necessary!
2329
            require_once("$CFG->libdir/gradelib.php");
2330
 
2331
            // remove all remaining roles
2332
            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2333
 
2334
            //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2335
            groups_delete_group_members($courseid, $userid);
2336
 
2337
            grade_user_unenrol($courseid, $userid);
2338
 
2339
            $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2340
 
2341
            $ue->lastenrol = true; // means user not enrolled any more
2342
        }
2343
        // Trigger event.
2344
        $event = \core\event\user_enrolment_deleted::create(
2345
                array(
2346
                    'courseid' => $courseid,
2347
                    'context' => $context,
2348
                    'relateduserid' => $ue->userid,
2349
                    'objectid' => $ue->id,
2350
                    'other' => array(
2351
                        'userenrolment' => (array)$ue,
2352
                        'enrol' => $name
2353
                        )
2354
                    )
2355
                );
2356
        $event->trigger();
2357
        // User enrolments have changed, so mark user as dirty.
2358
        mark_user_dirty($userid);
2359
 
2360
        // Check if courrse contacts cache needs to be cleared.
2361
        core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2362
 
2363
        // reset current user enrolment caching
2364
        if ($userid == $USER->id) {
2365
            if (isset($USER->enrol['enrolled'][$courseid])) {
2366
                unset($USER->enrol['enrolled'][$courseid]);
2367
            }
2368
            if (isset($USER->enrol['tempguest'][$courseid])) {
2369
                unset($USER->enrol['tempguest'][$courseid]);
2370
                remove_temp_course_roles($context);
2371
            }
2372
        }
2373
    }
2374
 
2375
    /**
2376
     * Forces synchronisation of user enrolments.
2377
     *
2378
     * This is important especially for external enrol plugins,
2379
     * this function is called for all enabled enrol plugins
2380
     * right after every user login.
2381
     *
2382
     * @param object $user user record
2383
     * @return void
2384
     */
2385
    public function sync_user_enrolments($user) {
2386
        // override if necessary
2387
    }
2388
 
2389
    /**
2390
     * This returns false for backwards compatibility, but it is really recommended.
2391
     *
2392
     * @since Moodle 3.1
2393
     * @return boolean
2394
     */
2395
    public function use_standard_editing_ui() {
2396
        return false;
2397
    }
2398
 
2399
    /**
2400
     * Return whether or not, given the current state, it is possible to add a new instance
2401
     * of this enrolment plugin to the course.
2402
     *
2403
     * Default implementation is just for backwards compatibility.
2404
     *
2405
     * @param int $courseid
2406
     * @return boolean
2407
     */
2408
    public function can_add_instance($courseid) {
2409
        $link = $this->get_newinstance_link($courseid);
2410
        return !empty($link);
2411
    }
2412
 
2413
    /**
2414
     * Return whether or not, given the current state, it is possible to edit an instance
2415
     * of this enrolment plugin in the course. Used by the standard editing UI
2416
     * to generate a link to the edit instance form if editing is allowed.
2417
     *
2418
     * @param stdClass $instance
2419
     * @return boolean
2420
     */
2421
    public function can_edit_instance($instance) {
2422
        $context = context_course::instance($instance->courseid);
2423
 
2424
        return has_capability('enrol/' . $instance->enrol . ':config', $context);
2425
    }
2426
 
2427
    /**
2428
     * Returns link to page which may be used to add new instance of enrolment plugin in course.
2429
     * @param int $courseid
2430
     * @return ?moodle_url page url
2431
     */
2432
    public function get_newinstance_link($courseid) {
2433
        // override for most plugins, check if instance already exists in cases only one instance is supported
2434
        return NULL;
2435
    }
2436
 
2437
    /**
2438
     * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
2439
     */
2440
    public function instance_deleteable($instance) {
2441
        throw new coding_exception('Function enrol_plugin::instance_deleteable() is deprecated, use
2442
                enrol_plugin::can_delete_instance() instead');
2443
    }
2444
 
2445
    /**
2446
     * Is it possible to delete enrol instance via standard UI?
2447
     *
2448
     * @param stdClass  $instance
2449
     * @return bool
2450
     */
2451
    public function can_delete_instance($instance) {
2452
        return false;
2453
    }
2454
 
2455
    /**
2456
     * Is it possible to hide/show enrol instance via standard UI?
2457
     *
2458
     * @param stdClass $instance
2459
     * @return bool
2460
     */
2461
    public function can_hide_show_instance($instance) {
2462
        debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2463
        return true;
2464
    }
2465
 
2466
    /**
2467
     * Returns link to manual enrol UI if exists.
2468
     * Does the access control tests automatically.
2469
     *
2470
     * @param object $instance
2471
     * @return ?moodle_url
2472
     */
2473
    public function get_manual_enrol_link($instance) {
2474
        return NULL;
2475
    }
2476
 
2477
    /**
2478
     * Returns list of unenrol links for all enrol instances in course.
2479
     *
2480
     * @param stdClass $instance
2481
     * @return ?moodle_url or NULL if self unenrolment not supported
2482
     */
2483
    public function get_unenrolself_link($instance) {
2484
        global $USER, $CFG, $DB;
2485
 
2486
        $name = $this->get_name();
2487
        if ($instance->enrol !== $name) {
2488
            throw new coding_exception('invalid enrol instance!');
2489
        }
2490
 
2491
        if ($instance->courseid == SITEID) {
2492
            return NULL;
2493
        }
2494
 
2495
        if (!enrol_is_enabled($name)) {
2496
            return NULL;
2497
        }
2498
 
2499
        if ($instance->status != ENROL_INSTANCE_ENABLED) {
2500
            return NULL;
2501
        }
2502
 
2503
        if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2504
            return NULL;
2505
        }
2506
 
2507
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2508
 
2509
        if (!has_capability("enrol/$name:unenrolself", $context)) {
2510
            return NULL;
2511
        }
2512
 
2513
        if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2514
            return NULL;
2515
        }
2516
 
2517
        return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2518
    }
2519
 
2520
    /**
2521
     * Adds enrol instance UI to course edit form
2522
     *
2523
     * @param object $instance enrol instance or null if does not exist yet
2524
     * @param MoodleQuickForm $mform
2525
     * @param object $data
2526
     * @param object $context context of existing course or parent category if course does not exist
2527
     * @return void
2528
     */
2529
    public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2530
        // override - usually at least enable/disable switch, has to add own form header
2531
    }
2532
 
2533
    /**
2534
     * Adds form elements to add/edit instance form.
2535
     *
2536
     * @since Moodle 3.1
2537
     * @param object $instance enrol instance or null if does not exist yet
2538
     * @param MoodleQuickForm $mform
2539
     * @param context $context
2540
     * @return void
2541
     */
2542
    public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2543
        // Do nothing by default.
2544
    }
2545
 
2546
    /**
2547
     * Perform custom validation of the data used to edit the instance.
2548
     *
2549
     * @since Moodle 3.1
2550
     * @param array $data array of ("fieldname"=>value) of submitted data
2551
     * @param array $files array of uploaded files "element_name"=>tmp_file_path
2552
     * @param object $instance The instance data loaded from the DB.
2553
     * @param context $context The context of the instance we are editing
2554
     * @return array of "element_name"=>"error_description" if there are errors,
2555
     *         or an empty array if everything is OK.
2556
     */
2557
    public function edit_instance_validation($data, $files, $instance, $context) {
2558
        // No errors by default.
2559
        debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2560
        return array();
2561
    }
2562
 
2563
    /**
2564
     * Validates course edit form data
2565
     *
2566
     * @param object $instance enrol instance or null if does not exist yet
2567
     * @param array $data
2568
     * @param object $context context of existing course or parent category if course does not exist
2569
     * @return array errors array
2570
     */
2571
    public function course_edit_validation($instance, array $data, $context) {
2572
        return array();
2573
    }
2574
 
2575
    /**
2576
     * Called after updating/inserting course.
2577
     *
2578
     * @param bool $inserted true if course just inserted
2579
     * @param object $course
2580
     * @param object $data form data
2581
     * @return void
2582
     */
2583
    public function course_updated($inserted, $course, $data) {
2584
        if ($inserted) {
2585
            if ($this->get_config('defaultenrol')) {
2586
                $this->add_default_instance($course);
2587
            }
2588
        }
2589
    }
2590
 
2591
    /**
2592
     * Add new instance of enrol plugin.
2593
     * @param object $course
2594
     * @param array instance fields
2595
     * @return int id of new instance, null if can not be created
2596
     */
2597
    public function add_instance($course, array $fields = NULL) {
2598
        global $DB;
2599
 
2600
        if ($course->id == SITEID) {
2601
            throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2602
        }
2603
 
2604
        $instance = new stdClass();
2605
        $instance->enrol          = $this->get_name();
2606
        $instance->status         = ENROL_INSTANCE_ENABLED;
2607
        $instance->courseid       = $course->id;
2608
        $instance->enrolstartdate = 0;
2609
        $instance->enrolenddate   = 0;
2610
        $instance->timemodified   = time();
2611
        $instance->timecreated    = $instance->timemodified;
2612
        $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2613
 
2614
        $fields = (array)$fields;
2615
        unset($fields['enrol']);
2616
        unset($fields['courseid']);
2617
        unset($fields['sortorder']);
2618
        foreach($fields as $field=>$value) {
2619
            $instance->$field = $value;
2620
        }
2621
 
2622
        $instance->id = $DB->insert_record('enrol', $instance);
2623
 
2624
        \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2625
 
2626
        return $instance->id;
2627
    }
2628
 
2629
    /**
2630
     * Update instance of enrol plugin.
2631
     *
2632
     * @since Moodle 3.1
2633
     * @param stdClass $instance
2634
     * @param stdClass $data modified instance fields
2635
     * @return boolean
2636
     */
2637
    public function update_instance($instance, $data) {
2638
        global $DB;
2639
        $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2640
                            'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2641
                            'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2642
                            'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2643
                            'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2644
                            'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2645
 
2646
        foreach ($properties as $key) {
2647
            if (isset($data->$key)) {
2648
                $instance->$key = $data->$key;
2649
            }
2650
        }
2651
        $instance->timemodified = time();
2652
 
2653
        $update = $DB->update_record('enrol', $instance);
2654
        if ($update) {
2655
            \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2656
        }
2657
        return $update;
2658
    }
2659
 
2660
    /**
2661
     * Add new instance of enrol plugin with default settings,
2662
     * called when adding new instance manually or when adding new course.
2663
     *
2664
     * Not all plugins support this.
2665
     *
2666
     * @param object $course
2667
     * @return ?int id of new instance or null if no default supported
2668
     */
2669
    public function add_default_instance($course) {
2670
        return null;
2671
    }
2672
 
2673
    /**
2674
     * Add new instance of enrol plugin with custom settings,
2675
     * called when adding new instance manually or when adding new course.
2676
     * Used for example on course upload.
2677
     *
2678
     * Not all plugins support this.
2679
     *
2680
     * @param stdClass $course Course object
2681
     * @param array|null $fields instance fields
2682
     * @return int|null id of new instance or null if not supported
2683
     */
2684
    public function add_custom_instance(stdClass $course, ?array $fields = null): ?int {
2685
        return null;
2686
    }
2687
 
2688
    /**
2689
     * Check if enrolment plugin is supported in csv course upload.
2690
     *
2691
     * If supported, plugins are also encouraged to override methods:
2692
     * {@see self::fill_enrol_custom_fields()}, {@see self::validate_plugin_data_context()}
2693
     *
2694
     * @return bool
2695
     */
2696
    public function is_csv_upload_supported(): bool {
2697
        return false;
2698
    }
2699
 
2700
    /**
2701
     * Update instance status
2702
     *
2703
     * Override when plugin needs to do some action when enabled or disabled.
2704
     *
2705
     * @param stdClass $instance
2706
     * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2707
     * @return void
2708
     */
2709
    public function update_status($instance, $newstatus) {
2710
        global $DB;
2711
 
2712
        $instance->status = $newstatus;
2713
        $DB->update_record('enrol', $instance);
2714
 
2715
        // Dispatch the hook for post enrol status update actions.
2716
        $hook = new \core_enrol\hook\after_enrol_instance_status_updated(
2717
            enrolinstance: $instance,
2718
            newstatus: $newstatus,
2719
        );
2720
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2721
 
2722
        $context = context_course::instance($instance->courseid);
2723
        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2724
 
2725
        // Invalidate all enrol caches.
2726
        $context->mark_dirty();
2727
    }
2728
 
2729
    /**
2730
     * Update instance members.
2731
     *
2732
     * Update communication room membership for an instance action being performed.
2733
     *
2734
     * @param int $enrolmentinstanceid ID of the enrolment instance
2735
     * @param string $action The update action being performed
2736
     * @param stdClass $course The course object
2737
     * @return void
2738
     * @deprecated Since Moodle 4.4.0.
2739
     * @see \core_communication\hook_listener::update_communication_memberships_for_enrol_status_change()
2740
     * @todo MDL-80491 Final deprecation in Moodle 4.8.
2741
     *
2742
     */
2743
    public function update_communication(int $enrolmentinstanceid, string $action, stdClass $course): void {
2744
        debugging('Use of method update_communication is deprecated. This feature has been moved to
2745
        core_communication as a part of hooks api implementation so that plugins or core does not need to call this method anymore.
2746
        Method update_communication_memberships_for_enrol_status_change method in communication/classes/hook_listener.php
2747
        now handles all the operations related to this method using hooks callback recorded in lib/db/hooks.php.', DEBUG_DEVELOPER);
2748
        return;
2749
    }
2750
 
2751
    /**
2752
     * Delete course enrol plugin instance, unenrol all users.
2753
     * @param object $instance
2754
     * @return void
2755
     */
2756
    public function delete_instance($instance) {
2757
        global $DB;
2758
 
2759
        $name = $this->get_name();
2760
        if ($instance->enrol !== $name) {
2761
            throw new coding_exception('invalid enrol instance!');
2762
        }
2763
 
2764
        // Dispatch the hook for pre enrol instance delete actions.
2765
        $hook = new \core_enrol\hook\before_enrol_instance_deleted(
2766
            enrolinstance: $instance,
2767
        );
2768
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2769
 
2770
        //first unenrol all users
2771
        $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2772
        foreach ($participants as $participant) {
2773
            $this->unenrol_user($instance, $participant->userid);
2774
        }
2775
        $participants->close();
2776
 
2777
        // now clean up all remainders that were not removed correctly
2778
        if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2779
            foreach ($gms as $gm) {
2780
                groups_remove_member($gm->groupid, $gm->userid);
2781
            }
2782
        }
2783
        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2784
        $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2785
 
2786
        // finally drop the enrol row
2787
        $DB->delete_records('enrol', array('id'=>$instance->id));
2788
 
2789
        $context = context_course::instance($instance->courseid);
2790
        \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2791
 
2792
        // Invalidate all enrol caches.
2793
        $context->mark_dirty();
2794
    }
2795
 
2796
    /**
2797
     * Creates course enrol form, checks if form submitted
2798
     * and enrols user if necessary. It can also redirect.
2799
     *
2800
     * @param stdClass $instance
2801
     * @return string html text, usually a form in a text box
2802
     */
2803
    public function enrol_page_hook(stdClass $instance) {
2804
        return null;
2805
    }
2806
 
2807
    /**
2808
     * Checks if user can self enrol.
2809
     *
2810
     * @param stdClass $instance enrolment instance
2811
     * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2812
     *             used by navigation to improve performance.
2813
     * @return bool|string true if successful, else error message or false
2814
     */
2815
    public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2816
        return false;
2817
    }
2818
 
2819
    /**
2820
     * Return information for enrolment instance containing list of parameters required
2821
     * for enrolment, name of enrolment plugin etc.
2822
     *
2823
     * @param stdClass $instance enrolment instance
2824
     * @return stdClass|null instance info.
2825
     */
2826
    public function get_enrol_info(stdClass $instance) {
2827
        return null;
2828
    }
2829
 
2830
    /**
2831
     * Adds navigation links into course admin block.
2832
     *
2833
     * By defaults looks for manage links only.
2834
     *
2835
     * @param navigation_node $instancesnode
2836
     * @param stdClass $instance
2837
     * @return void
2838
     */
2839
    public function add_course_navigation($instancesnode, stdClass $instance) {
2840
        if ($this->use_standard_editing_ui()) {
2841
            $context = context_course::instance($instance->courseid);
2842
            $cap = 'enrol/' . $instance->enrol . ':config';
2843
            if (has_capability($cap, $context)) {
2844
                $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2845
                $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2846
                $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2847
            }
2848
        }
2849
    }
2850
 
2851
    /**
2852
     * Returns edit icons for the page with list of instances
2853
     * @param stdClass $instance
2854
     * @return array
2855
     */
2856
    public function get_action_icons(stdClass $instance) {
2857
        global $OUTPUT;
2858
 
2859
        $icons = array();
2860
        if ($this->use_standard_editing_ui()) {
2861
            $context = context_course::instance($instance->courseid);
2862
            $cap = 'enrol/' . $instance->enrol . ':config';
2863
            if (has_capability($cap, $context)) {
2864
                $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2865
                $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2866
                $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2867
                    array('class' => 'iconsmall')));
2868
            }
2869
        }
2870
        return $icons;
2871
    }
2872
 
2873
    /**
2874
     * Reads version.php and determines if it is necessary
2875
     * to execute the cron job now.
2876
     * @return bool
2877
     */
2878
    public function is_cron_required() {
2879
        global $CFG;
2880
 
2881
        $name = $this->get_name();
2882
        $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2883
        $plugin = new stdClass();
2884
        include($versionfile);
2885
        if (empty($plugin->cron)) {
2886
            return false;
2887
        }
2888
        $lastexecuted = $this->get_config('lastcron', 0);
2889
        if ($lastexecuted + $plugin->cron < time()) {
2890
            return true;
2891
        } else {
2892
            return false;
2893
        }
2894
    }
2895
 
2896
    /**
2897
     * Called for all enabled enrol plugins that returned true from is_cron_required().
2898
     * @return void
2899
     */
2900
    public function cron() {
2901
    }
2902
 
2903
    /**
2904
     * Called when user is about to be deleted
2905
     * @param object $user
2906
     * @return void
2907
     */
2908
    public function user_delete($user) {
2909
        global $DB;
2910
 
2911
        $sql = "SELECT e.*
2912
                  FROM {enrol} e
2913
                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2914
                 WHERE e.enrol = :name AND ue.userid = :userid";
2915
        $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2916
 
2917
        $rs = $DB->get_recordset_sql($sql, $params);
2918
        foreach($rs as $instance) {
2919
            $this->unenrol_user($instance, $user->id);
2920
        }
2921
        $rs->close();
2922
    }
2923
 
2924
    /**
2925
     * Returns an enrol_user_button that takes the user to a page where they are able to
2926
     * enrol users into the managers course through this plugin.
2927
     *
2928
     * Optional: If the plugin supports manual enrolments it can choose to override this
2929
     * otherwise it shouldn't
2930
     *
2931
     * @param course_enrolment_manager $manager
2932
     * @return enrol_user_button|false
2933
     */
2934
    public function get_manual_enrol_button(course_enrolment_manager $manager) {
2935
        return false;
2936
    }
2937
 
2938
    /**
2939
     * Gets an array of the user enrolment actions
2940
     *
2941
     * @param course_enrolment_manager $manager
2942
     * @param stdClass $ue
2943
     * @return array An array of user_enrolment_actions
2944
     */
2945
    public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2946
        $actions = [];
2947
        $context = $manager->get_context();
2948
        $instance = $ue->enrolmentinstance;
2949
        $params = $manager->get_moodlepage()->url->params();
2950
        $params['ue'] = $ue->id;
2951
 
2952
        // Edit enrolment action.
2953
        if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2954
            $title = get_string('editenrolment', 'enrol');
2955
            $icon = new pix_icon('t/edit', $title);
2956
            $url = new moodle_url('/enrol/editenrolment.php', $params);
2957
            $actionparams = [
2958
                'class' => 'editenrollink',
2959
                'rel' => $ue->id,
2960
                'data-action' => ENROL_ACTION_EDIT
2961
            ];
2962
            $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2963
        }
2964
 
2965
        // Unenrol action.
2966
        if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2967
            $title = get_string('unenrol', 'enrol');
2968
            $icon = new pix_icon('t/delete', $title);
2969
            $url = new moodle_url('/enrol/unenroluser.php', $params);
2970
            $actionparams = [
2971
                'class' => 'unenrollink',
2972
                'rel' => $ue->id,
2973
                'data-action' => ENROL_ACTION_UNENROL
2974
            ];
2975
            $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2976
        }
2977
        return $actions;
2978
    }
2979
 
2980
    /**
2981
     * Returns true if the plugin has one or more bulk operations that can be performed on
2982
     * user enrolments.
2983
     *
2984
     * @param course_enrolment_manager $manager
2985
     * @return bool
2986
     */
2987
    public function has_bulk_operations(course_enrolment_manager $manager) {
2988
       return false;
2989
    }
2990
 
2991
    /**
2992
     * Return an array of enrol_bulk_enrolment_operation objects that define
2993
     * the bulk actions that can be performed on user enrolments by the plugin.
2994
     *
2995
     * @param course_enrolment_manager $manager
2996
     * @return array
2997
     */
2998
    public function get_bulk_operations(course_enrolment_manager $manager) {
2999
        return array();
3000
    }
3001
 
3002
    /**
3003
     * Do any enrolments need expiration processing.
3004
     *
3005
     * Plugins that want to call this functionality must implement 'expiredaction' config setting.
3006
     *
3007
     * @param progress_trace $trace
3008
     * @param int $courseid one course, empty mean all
3009
     * @return bool true if any data processed, false if not
3010
     */
3011
    public function process_expirations(progress_trace $trace, $courseid = null) {
3012
        global $DB;
3013
 
3014
        $name = $this->get_name();
3015
        if (!enrol_is_enabled($name)) {
3016
            $trace->finished();
3017
            return false;
3018
        }
3019
 
3020
        $processed = false;
3021
        $params = array();
3022
        $coursesql = "";
3023
        if ($courseid) {
3024
            $coursesql = "AND e.courseid = :courseid";
3025
        }
3026
 
3027
        // Deal with expired accounts.
3028
        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
3029
 
3030
        if ($action == ENROL_EXT_REMOVED_UNENROL) {
3031
            $instances = array();
3032
            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3033
                      FROM {user_enrolments} ue
3034
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3035
                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3036
                     WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
3037
            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
3038
 
3039
            $rs = $DB->get_recordset_sql($sql, $params);
3040
            foreach ($rs as $ue) {
3041
                if (!$processed) {
3042
                    $trace->output("Starting processing of enrol_$name expirations...");
3043
                    $processed = true;
3044
                }
3045
                if (empty($instances[$ue->enrolid])) {
3046
                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3047
                }
3048
                $instance = $instances[$ue->enrolid];
3049
                if (!$this->roles_protected()) {
3050
                    // Let's just guess what extra roles are supposed to be removed.
3051
                    if ($instance->roleid) {
3052
                        role_unassign($instance->roleid, $ue->userid, $ue->contextid);
3053
                    }
3054
                }
3055
                // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
3056
                $this->unenrol_user($instance, $ue->userid);
3057
                $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
3058
            }
3059
            $rs->close();
3060
            unset($instances);
3061
 
3062
        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
3063
            $instances = array();
3064
            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3065
                      FROM {user_enrolments} ue
3066
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3067
                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3068
                     WHERE ue.timeend > 0 AND ue.timeend < :now
3069
                           AND ue.status = :useractive $coursesql";
3070
            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
3071
            $rs = $DB->get_recordset_sql($sql, $params);
3072
            foreach ($rs as $ue) {
3073
                if (!$processed) {
3074
                    $trace->output("Starting processing of enrol_$name expirations...");
3075
                    $processed = true;
3076
                }
3077
                if (empty($instances[$ue->enrolid])) {
3078
                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3079
                }
3080
                $instance = $instances[$ue->enrolid];
3081
 
3082
                if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
3083
                    if (!$this->roles_protected()) {
3084
                        // Let's just guess what roles should be removed.
3085
                        $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
3086
                        if ($count == 1) {
3087
                            role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
3088
 
3089
                        } else if ($count > 1 and $instance->roleid) {
3090
                            role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
3091
                        }
3092
                    }
3093
                    // In any case remove all roles that belong to this instance and user.
3094
                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
3095
                    // Final cleanup of subcontexts if there are no more course roles.
3096
                    if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
3097
                        role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
3098
                    }
3099
                }
3100
 
3101
                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
3102
                $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
3103
            }
3104
            $rs->close();
3105
            unset($instances);
3106
 
3107
        } else {
3108
            // ENROL_EXT_REMOVED_KEEP means no changes.
3109
        }
3110
 
3111
        if ($processed) {
3112
            $trace->output("...finished processing of enrol_$name expirations");
3113
        } else {
3114
            $trace->output("No expired enrol_$name enrolments detected");
3115
        }
3116
        $trace->finished();
3117
 
3118
        return $processed;
3119
    }
3120
 
3121
    /**
3122
     * Send expiry notifications.
3123
     *
3124
     * Plugin that wants to have expiry notification MUST implement following:
3125
     * - expirynotifyhour plugin setting,
3126
     * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
3127
     * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
3128
     *   expirymessageenrolledsubject and expirymessageenrolledbody),
3129
     * - expiry_notification provider in db/messages.php,
3130
     * - upgrade code that sets default thresholds for existing courses (should be 1 day),
3131
     * - something that calls this method, such as cron.
3132
     *
3133
     * @param progress_trace $trace (accepts bool for backwards compatibility only)
3134
     */
3135
    public function send_expiry_notifications($trace) {
3136
        global $CFG;
3137
 
3138
        $name = $this->get_name();
3139
        if (!enrol_is_enabled($name)) {
3140
            $trace->finished();
3141
            return;
3142
        }
3143
 
3144
        // Unfortunately this may take a long time, it should not be interrupted,
3145
        // otherwise users get duplicate notification.
3146
 
3147
        core_php_time_limit::raise();
3148
        raise_memory_limit(MEMORY_HUGE);
3149
 
3150
 
3151
        $expirynotifylast = $this->get_config('expirynotifylast', 0);
3152
        $expirynotifyhour = $this->get_config('expirynotifyhour');
3153
        if (is_null($expirynotifyhour)) {
3154
            debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
3155
            $trace->finished();
3156
            return;
3157
        }
3158
 
3159
        if (!($trace instanceof progress_trace)) {
3160
            $trace = $trace ? new text_progress_trace() : new null_progress_trace();
3161
            debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
3162
        }
3163
 
3164
        $timenow = time();
3165
        $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
3166
 
3167
        if ($expirynotifylast > $notifytime) {
3168
            $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
3169
            $trace->finished();
3170
            return;
3171
 
3172
        } else if ($timenow < $notifytime) {
3173
            $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
3174
            $trace->finished();
3175
            return;
3176
        }
3177
 
3178
        $trace->output('Processing '.$name.' enrolment expiration notifications...');
3179
 
3180
        // Notify users responsible for enrolment once every day.
3181
        $this->fetch_users_and_notify_expiry($timenow, $name, $trace);
3182
 
3183
        $trace->output('...notification processing finished.');
3184
        $trace->finished();
3185
 
3186
        $this->set_config('expirynotifylast', $timenow);
3187
    }
3188
 
3189
    /**
3190
     * Notify users about enrolment expiration.
3191
     *
3192
     * Retrieves enrolment data from the database and notifies users about their
3193
     * upcoming course enrolment expiration based on expiry thresholds and notification settings.
3194
     *
3195
     * @param int $timenow Current time.
3196
     * @param string $name Name of this enrol plugin.
3197
     * @param progress_trace $trace (accepts bool for backwards compatibility only).
3198
     * @return void
3199
     */
3200
    protected function fetch_users_and_notify_expiry(int $timenow, string $name, progress_trace $trace): void {
3201
        global $DB, $CFG;
3202
 
3203
        $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
3204
                  FROM {user_enrolments} ue
3205
                  JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
3206
                  JOIN {course} c ON (c.id = e.courseid)
3207
                  JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
3208
                 WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
3209
              ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
3210
        $params = [
3211
            'enabled' => ENROL_INSTANCE_ENABLED,
3212
            'active' => ENROL_USER_ACTIVE,
3213
            'now1' => $timenow,
3214
            'now2' => $timenow,
3215
            'name' => $name,
3216
        ];
3217
 
3218
        $rs = $DB->get_recordset_sql($sql, $params);
3219
 
3220
        $lastenrollid = 0;
3221
        $users = [];
3222
 
3223
        foreach ($rs as $ue) {
3224
            if ($lastenrollid && $lastenrollid != $ue->enrolid) {
3225
                $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3226
                $users = [];
3227
            }
3228
            $lastenrollid = $ue->enrolid;
3229
 
3230
            $enroller = $this->get_enroller($ue->enrolid);
3231
            $context = context_course::instance($ue->courseid);
3232
 
3233
            $user = $DB->get_record('user', ['id' => $ue->userid]);
3234
 
3235
            $users[] = [
3236
                'fullname' => fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)),
3237
                'timeend' => $ue->timeend,
3238
            ];
3239
 
3240
            if (!$ue->notifyall) {
3241
                continue;
3242
            }
3243
 
3244
            if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
3245
                // Notify enrolled users only once at the start of the threshold.
3246
                $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".
3247
                    userdate($ue->timeend, '', $CFG->timezone), 1);
3248
                continue;
3249
            }
3250
 
3251
            $this->notify_expiry_enrolled($user, $ue, $trace);
3252
        }
3253
        $rs->close();
3254
 
3255
        if ($lastenrollid && $users) {
3256
            $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3257
        }
3258
    }
3259
 
3260
    /**
3261
     * Returns the user who is responsible for enrolments for given instance.
3262
     *
3263
     * Override if plugin knows anybody better than admin.
3264
     *
3265
     * @param int $instanceid enrolment instance id
3266
     * @return stdClass user record
3267
     */
3268
    protected function get_enroller($instanceid) {
3269
        return get_admin();
3270
    }
3271
 
3272
    /**
3273
     * Notify user about incoming expiration of their enrolment,
3274
     * it is called only if notification of enrolled users (aka students) is enabled in course.
3275
     *
3276
     * This is executed only once for each expiring enrolment right
3277
     * at the start of the expiration threshold.
3278
     *
3279
     * @param stdClass $user
3280
     * @param stdClass $ue
3281
     * @param progress_trace $trace
3282
     */
3283
    protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3284
        global $CFG;
3285
 
3286
        $name = $this->get_name();
3287
 
3288
        $oldforcelang = force_current_language($user->lang);
3289
 
3290
        $enroller = $this->get_enroller($ue->enrolid);
3291
        $context = context_course::instance($ue->courseid);
3292
 
3293
        $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name);
3294
        $body = $this->get_expiry_message_body($user, $ue, $name, $enroller, $context);
3295
 
3296
        $coursename = format_string($ue->fullname, true, ['context' => $context]);
3297
 
3298
        $message = new \core\message\message();
3299
        $message->courseid          = $ue->courseid;
3300
        $message->notification      = 1;
3301
        $message->component         = 'enrol_'.$name;
3302
        $message->name              = 'expiry_notification';
3303
        $message->userfrom          = $enroller;
3304
        $message->userto            = $user;
3305
        $message->subject           = $subject;
3306
        $message->fullmessage       = $body;
3307
        $message->fullmessageformat = FORMAT_MARKDOWN;
3308
        $message->fullmessagehtml   = markdown_to_html($body);
3309
        $message->smallmessage      = $subject;
3310
        $message->contexturlname    = $coursename;
3311
        $message->contexturl        = (string)new moodle_url('/course/view.php', ['id' => $ue->courseid]);
3312
 
3313
        if (message_send($message)) {
3314
            $stringmessage = 'notifying user %s that enrolment in course %s expires on %s';
3315
        } else {
3316
            $stringmessage = 'error notifying user %s that enrolment in course %s expires on %s';
3317
        }
3318
        $outputmessage = sprintf($stringmessage, $ue->userid, $ue->courseid, userdate($ue->timeend, '', $CFG->timezone));
3319
        $trace->output($outputmessage, 1);
3320
 
3321
        force_current_language($oldforcelang);
3322
    }
3323
 
3324
    /**
3325
     * Generate subject and body messages for enrolment expiration notification.
3326
     *
3327
     * @param stdClass $user An object representing the user.
3328
     * @param stdClass $ue An object containing enrolment data.
3329
     * @param string $name Name of this enrol plugin.
3330
     * @param stdClass $enroller The user who is responsible for enrolments.
3331
     * @param context $context The context object.
3332
     * @return string Return the body message.
3333
     */
3334
    protected function get_expiry_message_body(stdClass $user, stdClass $ue, string $name,
3335
            stdClass $enroller, context $context): string {
3336
        $a = new stdClass();
3337
        $a->course   = format_string($ue->fullname, true, ['context' => $context]);
3338
        $a->user     = fullname($user, true);
3339
        $a->timeend  = userdate($ue->timeend, '', $user->timezone);
3340
        $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3341
        return get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3342
    }
3343
 
3344
    /**
3345
     * Notify person responsible for enrolments that some user enrolments will be expired soon,
3346
     * it is called only if notification of enrollers (aka teachers) is enabled in course.
3347
     *
3348
     * This is called repeatedly every day for each course if there are any pending expiration
3349
     * in the expiration threshold.
3350
     *
3351
     * @param int $eid
3352
     * @param array $users
3353
     * @param progress_trace $trace
3354
     */
3355
    protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3356
        global $DB;
3357
 
3358
        $name = $this->get_name();
3359
 
3360
        $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3361
        $context = context_course::instance($instance->courseid);
3362
        $course = $DB->get_record('course', array('id'=>$instance->courseid));
3363
 
3364
        $enroller = $this->get_enroller($instance->id);
3365
        $admin = get_admin();
3366
 
3367
        $oldforcelang = force_current_language($enroller->lang);
3368
 
3369
        foreach($users as $key=>$info) {
3370
            $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3371
        }
3372
 
3373
        $a = new stdClass();
3374
        $a->course    = format_string($course->fullname, true, array('context'=>$context));
3375
        $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3376
        $a->users     = implode("\n", $users);
3377
        $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3378
 
3379
        $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3380
        $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3381
 
3382
        $message = new \core\message\message();
3383
        $message->courseid          = $course->id;
3384
        $message->notification      = 1;
3385
        $message->component         = 'enrol_'.$name;
3386
        $message->name              = 'expiry_notification';
3387
        $message->userfrom          = $admin;
3388
        $message->userto            = $enroller;
3389
        $message->subject           = $subject;
3390
        $message->fullmessage       = $body;
3391
        $message->fullmessageformat = FORMAT_MARKDOWN;
3392
        $message->fullmessagehtml   = markdown_to_html($body);
3393
        $message->smallmessage      = $subject;
3394
        $message->contexturlname    = $a->course;
3395
        $message->contexturl        = $a->extendurl;
3396
 
3397
        if (message_send($message)) {
3398
            $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3399
        } else {
3400
            $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3401
        }
3402
 
3403
        force_current_language($oldforcelang);
3404
    }
3405
 
3406
    /**
3407
     * Backup execution step hook to annotate custom fields.
3408
     *
3409
     * @param backup_enrolments_execution_step $step
3410
     * @param stdClass $enrol
3411
     */
3412
    public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3413
        // Override as necessary to annotate custom fields in the enrol table.
3414
    }
3415
 
3416
    /**
3417
     * Automatic enrol sync executed during restore.
3418
     * Useful for automatic sync by course->idnumber or course category.
3419
     * @param stdClass $course course record
3420
     */
3421
    public function restore_sync_course($course) {
3422
        // Override if necessary.
3423
    }
3424
 
3425
    /**
3426
     * Restore instance and map settings.
3427
     *
3428
     * @param restore_enrolments_structure_step $step
3429
     * @param stdClass $data
3430
     * @param stdClass $course
3431
     * @param int $oldid
3432
     */
3433
    public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3434
        // Do not call this from overridden methods, restore and set new id there.
3435
        $step->set_mapping('enrol', $oldid, 0);
3436
    }
3437
 
3438
    /**
3439
     * Restore user enrolment.
3440
     *
3441
     * @param restore_enrolments_structure_step $step
3442
     * @param stdClass $data
3443
     * @param stdClass $instance
3444
     * @param int $oldinstancestatus
3445
     * @param int $userid
3446
     */
3447
    public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3448
        // Override as necessary if plugin supports restore of enrolments.
3449
    }
3450
 
3451
    /**
3452
     * Restore role assignment.
3453
     *
3454
     * @param stdClass $instance
3455
     * @param int $roleid
3456
     * @param int $userid
3457
     * @param int $contextid
3458
     */
3459
    public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3460
        // No role assignment by default, override if necessary.
3461
    }
3462
 
3463
    /**
3464
     * Restore user group membership.
3465
     * @param stdClass $instance
3466
     * @param int $groupid
3467
     * @param int $userid
3468
     */
3469
    public function restore_group_member($instance, $groupid, $userid) {
3470
        // Implement if you want to restore protected group memberships,
3471
        // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3472
    }
3473
 
3474
    /**
3475
     * Returns defaults for new instances.
3476
     * @since Moodle 3.1
3477
     * @return array
3478
     */
3479
    public function get_instance_defaults() {
3480
        return array();
3481
    }
3482
 
3483
    /**
3484
     * Validate a list of parameter names and types.
3485
     * @since Moodle 3.1
3486
     *
3487
     * @param array $data array of ("fieldname"=>value) of submitted data
3488
     * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3489
     * @return array of "element_name"=>"error_description" if there are errors,
3490
     *         or an empty array if everything is OK.
3491
     */
3492
    public function validate_param_types($data, $rules) {
3493
        $errors = array();
3494
        $invalidstr = get_string('invaliddata', 'error');
3495
        foreach ($rules as $fieldname => $rule) {
3496
            if (!array_key_exists($fieldname, $data)) {
3497
                continue;
3498
            }
3499
            if (is_array($rule)) {
3500
                if (!in_array($data[$fieldname], $rule)) {
3501
                    $errors[$fieldname] = $invalidstr;
3502
                }
3503
            } else {
3504
                if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3505
                    $errors[$fieldname] = $invalidstr;
3506
                }
3507
            }
3508
        }
3509
        return $errors;
3510
    }
3511
 
3512
    /**
3513
     * Fill custom fields data for a given enrolment plugin.
3514
     *
3515
     * For example: resolve linked entities from the idnumbers (cohort, role, group, etc.)
3516
     * Also fill the default values that are not specified.
3517
     *
3518
     * @param array $enrolmentdata enrolment data received in CSV file in tool_uploadcourse
3519
     * @param int $courseid Course ID.
3520
     * @return array Updated enrolment data with custom fields info.
3521
     */
3522
    public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid): array {
3523
        return $enrolmentdata;
3524
    }
3525
 
3526
    /**
3527
     * Check if data is valid for a given enrolment plugin
3528
     *
3529
     * @param array $enrolmentdata enrolment data to validate.
3530
     * @param int|null $courseid Course ID.
3531
     * @return array Errors
3532
     */
3533
    public function validate_enrol_plugin_data(array $enrolmentdata, ?int $courseid = null): array {
3534
        $errors = [];
3535
        if (!$this->is_csv_upload_supported()) {
3536
            $errors['errorunsupportedmethod'] =
3537
                new lang_string('errorunsupportedmethod', 'tool_uploadcourse',
3538
                    get_class($this));
3539
        } else {
3540
            $plugin = $this->get_name();
3541
            if (!enrol_is_enabled($plugin)) {
3542
                $pluginname = get_string('pluginname', 'enrol_' . $plugin);
3543
                $errors['plugindisabled'] = new lang_string('plugindisabled', 'enrol', $pluginname);
3544
            }
3545
        }
3546
        return $errors;
3547
    }
3548
 
3549
    /**
3550
     * Check if plugin custom data is allowed in relevant context.
3551
     *
3552
     * This is called from the tool_uploadcourse if the plugin supports instance creation in
3553
     * upload course ({@see self::is_csv_upload_supported()})
3554
     *
3555
     * Override it if plugin can validate provided data in relevant context.
3556
     *
3557
     * @param array $enrolmentdata enrolment data to validate.
3558
     * @param int|null $courseid Course ID.
3559
     * @return lang_string|null Error
3560
     */
3561
    public function validate_plugin_data_context(array $enrolmentdata, ?int $courseid = null): ?lang_string {
3562
        return null;
3563
    }
3564
 
3565
    /**
3566
     * Finds matching instances for a given course.
3567
     *
3568
     * @param array $enrolmentdata enrolment data.
3569
     * @param int $courseid Course ID.
3570
     * @return stdClass|null Matching instance
3571
     */
3572
    public function find_instance(array $enrolmentdata, int $courseid): ?stdClass {
3573
 
3574
        // By default, we assume we can't uniquely identify an instance so better not update any.
3575
        // Plugins can override this if they can uniquely identify an instance.
3576
        return null;
3577
    }
3578
 
3579
    /**
3580
     * Get the "from" contact which the message will be sent from.
3581
     *
3582
     * @param int $sendoption send email from constant ENROL_SEND_EMAIL_FROM_*
3583
     * @param context $context where the user will be fetched from.
3584
     * @return null|stdClass the contact user object.
3585
     */
3586
    public function get_welcome_message_contact(
3587
        int $sendoption,
3588
        context $context,
3589
    ): ?stdClass {
3590
        global $CFG;
3591
 
3592
        $acceptedsendoptions = [
3593
            ENROL_DO_NOT_SEND_EMAIL,
3594
            ENROL_SEND_EMAIL_FROM_COURSE_CONTACT,
3595
            ENROL_SEND_EMAIL_FROM_KEY_HOLDER,
3596
            ENROL_SEND_EMAIL_FROM_NOREPLY,
3597
        ];
3598
        if (!in_array($sendoption, $acceptedsendoptions)) {
3599
            throw new coding_exception('Invalid send option');
3600
        }
3601
        if ($sendoption === ENROL_DO_NOT_SEND_EMAIL) {
3602
            return null;
3603
        }
3604
        $contact = null;
3605
        // Send as the first user assigned as the course contact.
3606
        if ($sendoption === ENROL_SEND_EMAIL_FROM_COURSE_CONTACT) {
3607
            $rusers = [];
3608
            if (!empty($CFG->coursecontact)) {
3609
                $croles = explode(',', $CFG->coursecontact);
3610
                [$sort, $sortparams] = users_order_by_sql('u');
3611
                // We only use the first user.
3612
                $i = 0;
3613
                do {
3614
                    $userfieldsapi = \core_user\fields::for_name();
3615
                    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
3616
                    $rusers = get_role_users($croles[$i], $context, true, 'u.id,  u.confirmed, u.username, '. $allnames . ',
3617
                    u.email, r.sortorder, ra.id AS raid', 'r.sortorder, ra.id ASC, ' . $sort, null, '', '', '', '', $sortparams);
3618
                    $i++;
3619
                } while (empty($rusers) && !empty($croles[$i]));
3620
            }
3621
            if ($rusers) {
3622
                $contact = array_values($rusers)[0];
3623
            }
3624
        } else if ($sendoption === ENROL_SEND_EMAIL_FROM_KEY_HOLDER) {
3625
            // Send as the first user with enrol/self:holdkey capability assigned in the course.
3626
            [$sort] = users_order_by_sql('u');
3627
            $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', 'u.*', $sort);
3628
            if (!empty($keyholders)) {
3629
                $contact = array_values($keyholders)[0];
3630
            }
3631
        }
3632
 
3633
        if ($sendoption === ENROL_SEND_EMAIL_FROM_NOREPLY) {
3634
            $contact = core_user::get_noreply_user();
3635
        }
3636
 
3637
        return $contact;
3638
    }
3639
 
3640
    /**
3641
     * Send course welcome message to user.
3642
     *
3643
     * @param stdClass $instance Enrol instance.
3644
     * @param int $userid User ID.
3645
     * @param int $sendoption Send email from constant ENROL_SEND_EMAIL_FROM_*
3646
     * @param null|string $message Message to send to the user.
3647
     */
3648
    public function send_course_welcome_message_to_user(
3649
        stdClass $instance,
3650
        int $userid,
3651
        int $sendoption,
3652
        ?string $message = '',
3653
    ): void {
3654
        global $DB;
3655
        $context = context_course::instance($instance->courseid);
3656
        $user = core_user::get_user($userid);
3657
        $course = get_course($instance->courseid);
3658
        $courserole = $DB->get_field(
3659
            table: 'role',
3660
            return: 'shortname',
3661
            conditions: ['id' => $instance->roleid],
3662
        );
3663
 
3664
        $a = new stdClass();
11 efrain 3665
        $a->coursename = format_string($course->fullname, true, ['context' => $context, 'escape' => false]);
1 efrain 3666
        $a->profileurl = (new moodle_url(
3667
            url: '/user/view.php',
3668
            params: [
3669
                'id' => $user->id,
3670
                'course' => $instance->courseid,
3671
            ],
3672
        ))->out();
3673
        $a->fullname = fullname($user);
3674
 
3675
        if ($message && trim($message) !== '') {
3676
            $placeholders = [
3677
                '{$a->coursename}',
3678
                '{$a->profileurl}',
3679
                '{$a->fullname}',
3680
                '{$a->email}',
3681
                '{$a->firstname}',
3682
                '{$a->lastname}',
3683
                '{$a->courserole}',
3684
            ];
3685
            $values = [
3686
                $a->coursename,
3687
                $a->profileurl,
3688
                fullname($user),
3689
                $user->email,
3690
                $user->firstname,
3691
                $user->lastname,
3692
                $courserole,
3693
            ];
3694
            $message = str_replace($placeholders, $values, $message);
3695
            if (strpos($message, '<') === false) {
3696
                // Plain text only.
3697
                $messagetext = $message;
3698
                $messagehtml = text_to_html($messagetext, null, false, true);
3699
            } else {
3700
                // This is most probably the tag/newline soup known as FORMAT_MOODLE.
3701
                $messagehtml = format_text($message, FORMAT_MOODLE,
3702
                    ['context' => $context, 'para' => false, 'newlines' => true, 'filter' => true]);
3703
                $messagetext = html_to_text($messagehtml);
3704
            }
3705
        } else {
3706
            $messagetext = get_string('customwelcomemessageplaceholder', 'core_enrol', $a);
3707
            $messagehtml = text_to_html($messagetext, null, false, true);
3708
        }
3709
 
3710
        $contact = $this->get_welcome_message_contact(
3711
            sendoption: $sendoption,
3712
            context: $context,
3713
        );
3714
        if (!$contact) {
3715
            // Cannot find the contact to send the message from.
3716
            return;
3717
        }
3718
 
3719
        $message = new \core\message\message();
3720
        $message->courseid = $instance->courseid;
3721
        $message->component = 'moodle';
3722
        $message->name = 'enrolcoursewelcomemessage';
3723
        $message->userfrom = $contact;
3724
        $message->userto = $user;
11 efrain 3725
        $message->subject = get_string('welcometocourse', 'moodle', $a->coursename);
1 efrain 3726
        $message->fullmessage = $messagetext;
3727
        $message->fullmessageformat = FORMAT_MARKDOWN;
3728
        $message->fullmessagehtml = $messagehtml;
3729
        $message->notification = 1;
3730
        $message->contexturl = $a->profileurl;
3731
        $message->contexturlname = $course->fullname;
3732
 
3733
        message_send($message);
3734
    }
3735
 
3736
    /**
3737
     * Updates enrol plugin instance with provided data.
3738
     * @param int $courseid Course ID.
3739
     * @param array $enrolmentdata enrolment data.
3740
     * @param stdClass $instance Instance to update.
3741
     *
3742
     * @return stdClass updated instance
3743
     */
3744
    public function update_enrol_plugin_data(int $courseid, array $enrolmentdata, stdClass $instance): stdClass {
3745
        global $DB;
3746
 
3747
        // Sort out the start, end and date.
3748
        $instance->enrolstartdate = (isset($enrolmentdata['startdate']) ? strtotime($enrolmentdata['startdate']) : 0);
3749
        $instance->enrolenddate = (isset($enrolmentdata['enddate']) ? strtotime($enrolmentdata['enddate']) : 0);
3750
 
3751
        // Is the enrolment period set?
3752
        if (!empty($enrolmentdata['enrolperiod'])) {
3753
            if (preg_match('/^\d+$/', $enrolmentdata['enrolperiod'])) {
3754
                $enrolmentdata['enrolperiod'] = (int)$enrolmentdata['enrolperiod'];
3755
            } else {
3756
                // Try and convert period to seconds.
3757
                $enrolmentdata['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $enrolmentdata['enrolperiod']);
3758
            }
3759
            $instance->enrolperiod = $enrolmentdata['enrolperiod'];
3760
        }
3761
        if ($instance->enrolstartdate > 0 && isset($enrolmentdata['enrolperiod'])) {
3762
            $instance->enrolenddate = $instance->enrolstartdate + $enrolmentdata['enrolperiod'];
3763
        }
3764
        if ($instance->enrolenddate > 0) {
3765
            $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
3766
        }
3767
        if ($instance->enrolenddate < $instance->enrolstartdate) {
3768
            $instance->enrolenddate = $instance->enrolstartdate;
3769
        }
3770
 
3771
        // Sort out the given role.
3772
        if (isset($enrolmentdata['role']) || isset($enrolmentdata['roleid'])) {
3773
            if (isset($enrolmentdata['role'])) {
3774
                $roleid = $DB->get_field('role', 'id', ['shortname' => $enrolmentdata['role']], MUST_EXIST);
3775
            } else {
3776
                $roleid = $enrolmentdata['roleid'];
3777
            }
3778
            $instance->roleid = $roleid;
3779
        }
3780
 
3781
        // Sort out custom instance name.
3782
        if (isset($enrolmentdata['name'])) {
3783
            $instance->name = $enrolmentdata['name'];
3784
        }
3785
        return $instance;
3786
    }
3787
}