Proyectos de Subversion Moodle

Rev

Rev 11 | | 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
1441 ariadna 99
 * @return enrol_plugin[] array of enrol plugins name=>instance
1 efrain 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
 
1441 ariadna 812
    // Note: we can not use DISTINCT + text fields due to MS limitations, that is why
1 efrain 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
 */
1441 ariadna 852
function enrol_get_course_info_icons($course, ?array $instances = NULL) {
1 efrain 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)";
1441 ariadna 1114
        $params['now1']    = $params['now2'] = \core\di::get(\core\clock::class)->time();
1 efrain 1115
        $params['active']  = ENROL_USER_ACTIVE;
1116
        $params['enabled'] = ENROL_INSTANCE_ENABLED;
1117
    } else {
1118
        $subwhere = "";
1119
    }
1120
 
1121
    $coursefields = 'c.' .join(',c.', $fields);
1122
    $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1123
    $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
1124
    $params['contextlevel'] = CONTEXT_COURSE;
1125
 
1441 ariadna 1126
    //note: we can not use DISTINCT + text fields due to MS limitations, that is why we have the subselect there
1 efrain 1127
    $sql = "SELECT $coursefields $ccselect
1128
              FROM {course} c
1129
              JOIN (SELECT DISTINCT e.courseid
1130
                      FROM {enrol} e
1131
                      JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
1132
                 $subwhere
1133
                   ) en ON (en.courseid = c.id)
1134
           $ccjoin
1135
             WHERE c.id <> " . SITEID . "
1136
          $orderby";
1137
    $params['userid']  = $userid;
1138
 
1139
    $courses = $DB->get_records_sql($sql, $params);
1140
 
1141
    return $courses;
1142
}
1143
 
1144
 
1145
 
1146
/**
1147
 * Called when user is about to be deleted.
1148
 * @param object $user
1149
 * @return void
1150
 */
1151
function enrol_user_delete($user) {
1152
    global $DB;
1153
 
1154
    $plugins = enrol_get_plugins(true);
1155
    foreach ($plugins as $plugin) {
1156
        $plugin->user_delete($user);
1157
    }
1158
 
1159
    // force cleanup of all broken enrolments
1160
    $DB->delete_records('user_enrolments', array('userid'=>$user->id));
1161
}
1162
 
1163
/**
1164
 * Called when course is about to be deleted.
1165
 * If a user id is passed, only enrolments that the user has permission to un-enrol will be removed,
1166
 * otherwise all enrolments in the course will be removed.
1167
 *
1168
 * @param stdClass $course
1169
 * @param int|null $userid
1170
 * @return void
1171
 */
1172
function enrol_course_delete($course, $userid = null) {
1173
    global $DB;
1174
 
1175
    $context = context_course::instance($course->id);
1176
    $instances = enrol_get_instances($course->id, false);
1177
    $plugins = enrol_get_plugins(true);
1178
 
1179
    if ($userid) {
1180
        // If the user id is present, include only course enrolment instances which allow manual unenrolment and
1181
        // the given user have a capability to perform unenrolment.
1182
        $instances = array_filter($instances, function($instance) use ($userid, $plugins, $context) {
1183
            $unenrolcap = "enrol/{$instance->enrol}:unenrol";
1184
            return $plugins[$instance->enrol]->allow_unenrol($instance) &&
1185
                has_capability($unenrolcap, $context, $userid);
1186
        });
1187
    }
1188
 
1189
    foreach ($instances as $instance) {
1190
        if (isset($plugins[$instance->enrol])) {
1191
            $plugins[$instance->enrol]->delete_instance($instance);
1192
        }
1193
        // low level delete in case plugin did not do it
1194
        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
1195
        $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1196
        $DB->delete_records('enrol', array('id'=>$instance->id));
1197
    }
1198
}
1199
 
1200
/**
1201
 * Try to enrol user via default internal auth plugin.
1202
 *
1203
 * For now this is always using the manual enrol plugin...
1204
 *
1205
 * @param $courseid
1206
 * @param $userid
1207
 * @param $roleid
1208
 * @param $timestart
1209
 * @param $timeend
1210
 * @return bool success
1211
 */
1212
function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
1213
    global $DB;
1214
 
1215
    //note: this is hardcoded to manual plugin for now
1216
 
1217
    if (!enrol_is_enabled('manual')) {
1218
        return false;
1219
    }
1220
 
1221
    if (!$enrol = enrol_get_plugin('manual')) {
1222
        return false;
1223
    }
1224
    if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
1225
        return false;
1226
    }
1227
 
1228
    if ($roleid && !$DB->record_exists('role', ['id' => $roleid])) {
1229
        return false;
1230
    }
1231
 
1232
    $instance = reset($instances);
1233
    $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
1234
 
1235
    return true;
1236
}
1237
 
1238
/**
1239
 * Is there a chance users might self enrol
1240
 * @param int $courseid
1241
 * @return bool
1242
 */
1243
function enrol_selfenrol_available($courseid) {
1244
    $result = false;
1245
 
1246
    $plugins = enrol_get_plugins(true);
1247
    $enrolinstances = enrol_get_instances($courseid, true);
1248
    foreach($enrolinstances as $instance) {
1249
        if (!isset($plugins[$instance->enrol])) {
1250
            continue;
1251
        }
1252
        if ($instance->enrol === 'guest') {
1253
            continue;
1254
        }
1255
        if ((isguestuser() || !isloggedin()) &&
1256
            ($plugins[$instance->enrol]->is_self_enrol_available($instance) === true)) {
1257
            $result = true;
1258
            break;
1259
        }
1260
        if ($plugins[$instance->enrol]->show_enrolme_link($instance) === true) {
1261
            $result = true;
1262
            break;
1263
        }
1264
    }
1265
 
1266
    return $result;
1267
}
1268
 
1269
/**
1270
 * This function returns the end of current active user enrolment.
1271
 *
1272
 * It deals correctly with multiple overlapping user enrolments.
1273
 *
1274
 * @param int $courseid
1275
 * @param int $userid
1276
 * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
1277
 */
1278
function enrol_get_enrolment_end($courseid, $userid) {
1279
    global $DB;
1280
 
1281
    $sql = "SELECT ue.*
1282
              FROM {user_enrolments} ue
1283
              JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1284
              JOIN {user} u ON u.id = ue.userid
1285
             WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
1286
    $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
1287
 
1288
    if (!$enrolments = $DB->get_records_sql($sql, $params)) {
1289
        return false;
1290
    }
1291
 
1292
    $changes = array();
1293
 
1294
    foreach ($enrolments as $ue) {
1295
        $start = (int)$ue->timestart;
1296
        $end = (int)$ue->timeend;
1297
        if ($end != 0 and $end < $start) {
1298
            debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
1299
            continue;
1300
        }
1301
        if (isset($changes[$start])) {
1302
            $changes[$start] = $changes[$start] + 1;
1303
        } else {
1304
            $changes[$start] = 1;
1305
        }
1306
        if ($end === 0) {
1307
            // no end
1308
        } else if (isset($changes[$end])) {
1309
            $changes[$end] = $changes[$end] - 1;
1310
        } else {
1311
            $changes[$end] = -1;
1312
        }
1313
    }
1314
 
1315
    // let's sort then enrolment starts&ends and go through them chronologically,
1316
    // looking for current status and the next future end of enrolment
1317
    ksort($changes);
1318
 
1319
    $now = time();
1320
    $current = 0;
1321
    $present = null;
1322
 
1323
    foreach ($changes as $time => $change) {
1324
        if ($time > $now) {
1325
            if ($present === null) {
1326
                // we have just went past current time
1327
                $present = $current;
1328
                if ($present < 1) {
1329
                    // no enrolment active
1330
                    return false;
1331
                }
1332
            }
1333
            if ($present !== null) {
1334
                // we are already in the future - look for possible end
1335
                if ($current + $change < 1) {
1336
                    return $time;
1337
                }
1338
            }
1339
        }
1340
        $current += $change;
1341
    }
1342
 
1343
    if ($current > 0) {
1344
        return 0;
1345
    } else {
1346
        return false;
1347
    }
1348
}
1349
 
1350
/**
1351
 * Is current user accessing course via this enrolment method?
1352
 *
1353
 * This is intended for operations that are going to affect enrol instances.
1354
 *
1355
 * @param stdClass $instance enrol instance
1356
 * @return bool
1357
 */
1358
function enrol_accessing_via_instance(stdClass $instance) {
1359
    global $DB, $USER;
1360
 
1361
    if (empty($instance->id)) {
1362
        return false;
1363
    }
1364
 
1365
    if (is_siteadmin()) {
1366
        // Admins may go anywhere.
1367
        return false;
1368
    }
1369
 
1370
    return $DB->record_exists('user_enrolments', array('userid'=>$USER->id, 'enrolid'=>$instance->id));
1371
}
1372
 
1373
/**
1374
 * Returns true if user is enrolled (is participating) in course
1375
 * this is intended for students and teachers.
1376
 *
1377
 * Since 2.2 the result for active enrolments and current user are cached.
1378
 *
1379
 * @param context $context
1380
 * @param int|stdClass $user if null $USER is used, otherwise user object or id expected
1381
 * @param string $withcapability extra capability name
1382
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1383
 * @return bool
1384
 */
1385
function is_enrolled(context $context, $user = null, $withcapability = '', $onlyactive = false) {
1386
    global $USER, $DB;
1387
 
1388
    // First find the course context.
1389
    $coursecontext = $context->get_course_context();
1390
 
1391
    // Make sure there is a real user specified.
1392
    if ($user === null) {
1393
        $userid = isset($USER->id) ? $USER->id : 0;
1394
    } else {
1395
        $userid = is_object($user) ? $user->id : $user;
1396
    }
1397
 
1398
    if (empty($userid)) {
1399
        // Not-logged-in!
1400
        return false;
1401
    } else if (isguestuser($userid)) {
1402
        // Guest account can not be enrolled anywhere.
1403
        return false;
1404
    }
1405
 
1406
    // Note everybody participates on frontpage, so for other contexts...
1407
    if ($coursecontext->instanceid != SITEID) {
1408
        // Try cached info first - the enrolled flag is set only when active enrolment present.
1409
        if ($USER->id == $userid) {
1410
            $coursecontext->reload_if_dirty();
1411
            if (isset($USER->enrol['enrolled'][$coursecontext->instanceid])) {
1412
                if ($USER->enrol['enrolled'][$coursecontext->instanceid] > time()) {
1413
                    if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1414
                        return false;
1415
                    }
1416
                    return true;
1417
                }
1418
            }
1419
        }
1420
 
1421
        if ($onlyactive) {
1422
            // Look for active enrolments only.
1423
            $until = enrol_get_enrolment_end($coursecontext->instanceid, $userid);
1424
 
1425
            if ($until === false) {
1426
                return false;
1427
            }
1428
 
1429
            if ($USER->id == $userid) {
1430
                if ($until == 0) {
1431
                    $until = ENROL_MAX_TIMESTAMP;
1432
                }
1433
                $USER->enrol['enrolled'][$coursecontext->instanceid] = $until;
1434
                if (isset($USER->enrol['tempguest'][$coursecontext->instanceid])) {
1435
                    unset($USER->enrol['tempguest'][$coursecontext->instanceid]);
1436
                    remove_temp_course_roles($coursecontext);
1437
                }
1438
            }
1439
 
1440
        } else {
1441
            // Any enrolment is good for us here, even outdated, disabled or inactive.
1442
            $sql = "SELECT 'x'
1443
                      FROM {user_enrolments} ue
1444
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
1445
                      JOIN {user} u ON u.id = ue.userid
1446
                     WHERE ue.userid = :userid AND u.deleted = 0";
1447
            $params = array('userid' => $userid, 'courseid' => $coursecontext->instanceid);
1448
            if (!$DB->record_exists_sql($sql, $params)) {
1449
                return false;
1450
            }
1451
        }
1452
    }
1453
 
1454
    if ($withcapability and !has_capability($withcapability, $context, $userid)) {
1455
        return false;
1456
    }
1457
 
1458
    return true;
1459
}
1460
 
1461
/**
1462
 * Returns an array of joins, wheres and params that will limit the group of
1463
 * users to only those enrolled and with given capability (if specified).
1464
 *
1465
 * Note this join will return duplicate rows for users who have been enrolled
1466
 * several times (e.g. as manual enrolment, and as self enrolment). You may
1467
 * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
1468
 *
1469
 * In case is guaranteed some of the joins never match any rows, the resulting
1470
 * join_sql->cannotmatchanyrows will be true. This happens when the capability
1471
 * is prohibited.
1472
 *
1473
 * @param context $context
1474
 * @param string $prefix optional, a prefix to the user id column
1475
 * @param string|array $capability optional, may include a capability name, or array of names.
1476
 *      If an array is provided then this is the equivalent of a logical 'OR',
1477
 *      i.e. the user needs to have one of these capabilities.
1478
 * @param int|array|null $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1479
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1480
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1481
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1482
 * @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
1483
 */
1484
function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $groupids = 0,
1485
        $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1486
    $uid = $prefix . 'u.id';
1487
    $joins = array();
1488
    $wheres = array();
1489
    $cannotmatchanyrows = false;
1490
 
1491
    $enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
1492
    $joins[] = $enrolledjoin->joins;
1493
    $wheres[] = $enrolledjoin->wheres;
1494
    $params = $enrolledjoin->params;
1495
    $cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
1496
 
1497
    if (!empty($capability)) {
1498
        $capjoin = get_with_capability_join($context, $capability, $uid);
1499
        $joins[] = $capjoin->joins;
1500
        $wheres[] = $capjoin->wheres;
1501
        $params = array_merge($params, $capjoin->params);
1502
        $cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
1503
    }
1504
 
1505
    if ($groupids) {
1506
        $groupjoin = groups_get_members_join($groupids, $uid, $context);
1507
        $joins[] = $groupjoin->joins;
1508
        $params = array_merge($params, $groupjoin->params);
1509
        if (!empty($groupjoin->wheres)) {
1510
            $wheres[] = $groupjoin->wheres;
1511
        }
1512
        $cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
1513
    }
1514
 
1515
    $joins = implode("\n", $joins);
1516
    $wheres[] = "{$prefix}u.deleted = 0";
1517
    $wheres = implode(" AND ", $wheres);
1518
 
1519
    return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
1520
}
1521
 
1522
/**
1523
 * Returns array with sql code and parameters returning all ids
1524
 * of users enrolled into course.
1525
 *
1526
 * This function is using 'eu[0-9]+_' prefix for table names and parameters.
1527
 *
1528
 * @param context $context
1529
 * @param string $withcapability
1530
 * @param int|array|null $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1531
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1532
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1533
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1534
 * @return array list($sql, $params)
1535
 */
1536
function get_enrolled_sql(context $context, $withcapability = '', $groupids = 0, $onlyactive = false, $onlysuspended = false,
1537
                          $enrolid = 0) {
1538
 
1539
    // Use unique prefix just in case somebody makes some SQL magic with the result.
1540
    static $i = 0;
1541
    $i++;
1542
    $prefix = 'eu' . $i . '_';
1543
 
1544
    $capjoin = get_enrolled_with_capabilities_join(
1545
            $context, $prefix, $withcapability, $groupids, $onlyactive, $onlysuspended, $enrolid);
1546
 
1547
    $sql = "SELECT DISTINCT {$prefix}u.id
1548
              FROM {user} {$prefix}u
1549
            $capjoin->joins
1550
             WHERE $capjoin->wheres";
1551
 
1552
    return array($sql, $capjoin->params);
1553
}
1554
 
1555
/**
1556
 * Returns array with sql joins and parameters returning all ids
1557
 * of users enrolled into course.
1558
 *
1559
 * This function is using 'ej[0-9]+_' prefix for table names and parameters.
1560
 *
1561
 * @throws coding_exception
1562
 *
1563
 * @param context $context
1564
 * @param string $useridcolumn User id column used the calling query, e.g. u.id
1565
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1566
 * @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
1567
 * @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
1568
 * @return \core\dml\sql_join Contains joins, wheres, params
1569
 */
1570
function get_enrolled_join(context $context, $useridcolumn, $onlyactive = false, $onlysuspended = false, $enrolid = 0) {
1571
    // Use unique prefix just in case somebody makes some SQL magic with the result.
1572
    static $i = 0;
1573
    $i++;
1574
    $prefix = 'ej' . $i . '_';
1575
 
1576
    // First find the course context.
1577
    $coursecontext = $context->get_course_context();
1578
 
1579
    $isfrontpage = ($coursecontext->instanceid == SITEID);
1580
 
1581
    if ($onlyactive && $onlysuspended) {
1582
        throw new coding_exception("Both onlyactive and onlysuspended are set, this is probably not what you want!");
1583
    }
1584
    if ($isfrontpage && $onlysuspended) {
1585
        throw new coding_exception("onlysuspended is not supported on frontpage; please add your own early-exit!");
1586
    }
1587
 
1588
    $joins  = array();
1589
    $wheres = array();
1590
    $params = array();
1591
 
1592
    $wheres[] = "1 = 1"; // Prevent broken where clauses later on.
1593
 
1594
    // Note all users are "enrolled" on the frontpage, but for others...
1595
    if (!$isfrontpage) {
1596
        $where1 = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
1597
        $where2 = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
1598
 
1599
        $enrolconditions = array(
1600
            "{$prefix}e.id = {$prefix}ue.enrolid",
1601
            "{$prefix}e.courseid = :{$prefix}courseid",
1602
        );
1603
        if ($enrolid) {
1604
            $enrolconditions[] = "{$prefix}e.id = :{$prefix}enrolid";
1605
            $params[$prefix . 'enrolid'] = $enrolid;
1606
        }
1607
        $enrolconditionssql = implode(" AND ", $enrolconditions);
1608
        $ejoin = "JOIN {enrol} {$prefix}e ON ($enrolconditionssql)";
1609
 
1610
        $params[$prefix.'courseid'] = $coursecontext->instanceid;
1611
 
1612
        if (!$onlysuspended) {
1613
            $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = $useridcolumn";
1614
            $joins[] = $ejoin;
1615
            if ($onlyactive) {
1616
                $wheres[] = "$where1 AND $where2";
1617
            }
1618
        } else {
1619
            // Suspended only where there is enrolment but ALL are suspended.
1620
            // Consider multiple enrols where one is not suspended or plain role_assign.
1621
            $enrolselect = "SELECT DISTINCT {$prefix}ue.userid FROM {user_enrolments} {$prefix}ue $ejoin WHERE $where1 AND $where2";
1622
            $joins[] = "JOIN {user_enrolments} {$prefix}ue1 ON {$prefix}ue1.userid = $useridcolumn";
1623
            $enrolconditions = array(
1624
                "{$prefix}e1.id = {$prefix}ue1.enrolid",
1625
                "{$prefix}e1.courseid = :{$prefix}_e1_courseid",
1626
            );
1627
            if ($enrolid) {
1628
                $enrolconditions[] = "{$prefix}e1.id = :{$prefix}e1_enrolid";
1629
                $params[$prefix . 'e1_enrolid'] = $enrolid;
1630
            }
1631
            $enrolconditionssql = implode(" AND ", $enrolconditions);
1632
            $joins[] = "JOIN {enrol} {$prefix}e1 ON ($enrolconditionssql)";
1633
            $params["{$prefix}_e1_courseid"] = $coursecontext->instanceid;
1634
            $wheres[] = "$useridcolumn NOT IN ($enrolselect)";
1635
        }
1636
 
1637
        if ($onlyactive || $onlysuspended) {
1638
            $now = round(time(), -2); // Rounding helps caching in DB.
1639
            $params = array_merge($params, array($prefix . 'enabled' => ENROL_INSTANCE_ENABLED,
1640
                    $prefix . 'active' => ENROL_USER_ACTIVE,
1641
                    $prefix . 'now1' => $now, $prefix . 'now2' => $now));
1642
        }
1643
    }
1644
 
1645
    $joins = implode("\n", $joins);
1646
    $wheres = implode(" AND ", $wheres);
1647
 
1648
    return new \core\dml\sql_join($joins, $wheres, $params);
1649
}
1650
 
1651
/**
1652
 * Returns list of users enrolled into course.
1653
 *
1654
 * @param context $context
1655
 * @param string $withcapability
1656
 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1657
 * @param string $userfields requested user record fields
1658
 * @param string $orderby
1659
 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1660
 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1661
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1662
 * @return array of user records
1663
 */
1664
function get_enrolled_users(context $context, $withcapability = '', $groupids = 0, $userfields = 'u.*', $orderby = null,
1665
        $limitfrom = 0, $limitnum = 0, $onlyactive = false) {
1666
    global $DB;
1667
 
1668
    list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupids, $onlyactive);
1669
    $sql = "SELECT $userfields
1670
              FROM {user} u
1671
              JOIN ($esql) je ON je.id = u.id
1672
             WHERE u.deleted = 0";
1673
 
1674
    if ($orderby) {
1675
        $sql = "$sql ORDER BY $orderby";
1676
    } else {
1677
        list($sort, $sortparams) = users_order_by_sql('u');
1678
        $sql = "$sql ORDER BY $sort";
1679
        $params = array_merge($params, $sortparams);
1680
    }
1681
 
1682
    return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1683
}
1684
 
1685
/**
1686
 * Counts list of users enrolled into course (as per above function)
1687
 *
1688
 * @param context $context
1689
 * @param string $withcapability
1690
 * @param int|array $groupids The groupids, 0 or [] means all groups and USERSWITHOUTGROUP no group
1691
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1692
 * @return int number of users enrolled into course
1693
 */
1694
function count_enrolled_users(context $context, $withcapability = '', $groupids = 0, $onlyactive = false) {
1695
    global $DB;
1696
 
1697
    $capjoin = get_enrolled_with_capabilities_join(
1698
            $context, '', $withcapability, $groupids, $onlyactive);
1699
 
1700
    $sql = "SELECT COUNT(DISTINCT u.id)
1701
              FROM {user} u
1702
            $capjoin->joins
1703
             WHERE $capjoin->wheres AND u.deleted = 0";
1704
 
1705
    return $DB->count_records_sql($sql, $capjoin->params);
1706
}
1707
 
1708
/**
1709
 * Send welcome email "from" options.
1710
 *
1711
 * @return array list of from options
1712
 */
1713
function enrol_send_welcome_email_options() {
1714
    return [
1715
        ENROL_DO_NOT_SEND_EMAIL                 => get_string('no'),
1716
        ENROL_SEND_EMAIL_FROM_COURSE_CONTACT    => get_string('sendfromcoursecontact', 'enrol'),
1717
        ENROL_SEND_EMAIL_FROM_KEY_HOLDER        => get_string('sendfromkeyholder', 'enrol'),
1718
        ENROL_SEND_EMAIL_FROM_NOREPLY           => get_string('sendfromnoreply', 'enrol')
1719
    ];
1720
}
1721
 
1722
/**
1723
 * Serve the user enrolment form as a fragment.
1724
 *
1725
 * @param array $args List of named arguments for the fragment loader.
1726
 * @return string
1727
 */
1728
function enrol_output_fragment_user_enrolment_form($args) {
1729
    global $CFG, $DB;
1730
 
1731
    $args = (object) $args;
1732
    $context = $args->context;
1733
    require_capability('moodle/course:enrolreview', $context);
1734
 
1735
    $ueid = $args->ueid;
1736
    $userenrolment = $DB->get_record('user_enrolments', ['id' => $ueid], '*', MUST_EXIST);
1737
    $instance = $DB->get_record('enrol', ['id' => $userenrolment->enrolid], '*', MUST_EXIST);
1738
    $plugin = enrol_get_plugin($instance->enrol);
1739
    $customdata = [
1740
        'ue' => $userenrolment,
1741
        'modal' => true,
1742
        'enrolinstancename' => $plugin->get_instance_name($instance)
1743
    ];
1744
 
1745
    // Set the data if applicable.
1746
    $data = [];
1747
    if (isset($args->formdata)) {
1748
        $serialiseddata = json_decode($args->formdata);
1749
        parse_str($serialiseddata, $data);
1750
    }
1751
 
1752
    require_once("$CFG->dirroot/enrol/editenrolment_form.php");
1753
    $mform = new \enrol_user_enrolment_form(null, $customdata, 'post', '', null, true, $data);
1754
 
1755
    if (!empty($data)) {
1756
        $mform->set_data($data);
1757
        $mform->is_validated();
1758
    }
1759
 
1760
    return $mform->render();
1761
}
1762
 
1763
/**
1764
 * Returns the course where a user enrolment belong to.
1765
 *
1766
 * @param int $ueid user_enrolments id
1767
 * @return stdClass
1768
 */
1769
function enrol_get_course_by_user_enrolment_id($ueid) {
1770
    global $DB;
1771
    $sql = "SELECT c.* FROM {user_enrolments} ue
1772
              JOIN {enrol} e ON e.id = ue.enrolid
1773
              JOIN {course} c ON c.id = e.courseid
1774
             WHERE ue.id = :ueid";
1775
    return $DB->get_record_sql($sql, array('ueid' => $ueid));
1776
}
1777
 
1778
/**
1779
 * Return all users enrolled in a course.
1780
 *
1781
 * @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
1782
 * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
1783
 * @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
1784
 * @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
1785
 * @param array $usergroups Limit the results of users to the ones that belong to one of the submitted group ids.
1786
 * @return stdClass[]
1787
 */
1788
function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = [], $uefilter = [],
1789
                                $usergroups = []) {
1790
    global $DB;
1791
 
1792
    if (!$courseid && !$usersfilter && !$uefilter) {
1793
        throw new \coding_exception('You should specify at least 1 filter: courseid, users or user enrolments');
1794
    }
1795
 
1796
    $sql = "SELECT ue.id AS ueid, ue.status AS uestatus, ue.enrolid AS ueenrolid, ue.timestart AS uetimestart,
1797
             ue.timeend AS uetimeend, ue.modifierid AS uemodifierid, ue.timecreated AS uetimecreated,
1798
             ue.timemodified AS uetimemodified, e.status AS estatus,
1799
             u.* FROM {user_enrolments} ue
1800
              JOIN {enrol} e ON e.id = ue.enrolid
1801
              JOIN {user} u ON ue.userid = u.id
1802
             WHERE ";
1803
    $params = array();
1804
 
1805
    if ($courseid) {
1806
        $conditions[] = "e.courseid = :courseid";
1807
        $params['courseid'] = $courseid;
1808
    }
1809
 
1810
    if ($onlyactive) {
1811
        $conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
1812
            "(ue.timeend = 0 OR ue.timeend > :now2)";
1813
        // Improves db caching.
1814
        $params['now1']    = round(time(), -2);
1815
        $params['now2']    = $params['now1'];
1816
        $params['active']  = ENROL_USER_ACTIVE;
1817
        $params['enabled'] = ENROL_INSTANCE_ENABLED;
1818
    }
1819
 
1820
    if ($usersfilter) {
1821
        list($usersql, $userparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
1822
        $conditions[] = "ue.userid $usersql";
1823
        $params = $params + $userparams;
1824
    }
1825
 
1826
    if ($uefilter) {
1827
        list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
1828
        $conditions[] = "ue.id $uesql";
1829
        $params = $params + $ueparams;
1830
    }
1831
 
1832
    // Only select enrolled users that belong to a specific group(s).
1833
    if (!empty($usergroups)) {
1834
        $usergroups = array_map(function ($item) { // Sanitize groupid to int to be save for sql.
1835
            return (int)$item;
1836
        }, $usergroups);
1837
        list($ugsql, $ugparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
1838
        $conditions[] = 'ue.userid IN (SELECT userid FROM {groups_members} WHERE groupid ' . $ugsql . ')';
1839
        $params = $params + $ugparams;
1840
    }
1841
 
1842
    return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
1843
}
1844
 
1845
/**
1846
 * Get the list of options for the enrolment period dropdown
1847
 *
1848
 * @return array List of options for the enrolment period dropdown
1849
 */
1850
function enrol_get_period_list() {
1851
    $periodmenu = [];
1852
    $periodmenu[''] = get_string('unlimited');
1853
    for ($i = 1; $i <= 365; $i++) {
1854
        $seconds = $i * DAYSECS;
1855
        $periodmenu[$seconds] = get_string('numdays', '', $i);
1856
    }
1857
    return $periodmenu;
1858
}
1859
 
1860
/**
1861
 * Calculate duration base on start time and end time
1862
 *
1863
 * @param int $timestart Time start
1864
 * @param int $timeend Time end
1865
 * @return float|int Calculated duration
1866
 */
1867
function enrol_calculate_duration($timestart, $timeend) {
1868
    $duration = floor(($timeend - $timestart) / DAYSECS) * DAYSECS;
1869
    return $duration;
1870
}
1871
 
1872
/**
1873
 * Enrolment plugins abstract class.
1874
 *
1875
 * All enrol plugins should be based on this class,
1876
 * this is also the main source of documentation.
1877
 *
1878
 * @copyright  2010 Petr Skoda {@link http://skodak.org}
1879
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1880
 */
1881
abstract class enrol_plugin {
1882
    protected $config = null;
1883
 
1884
    /**
1885
     * Returns name of this enrol plugin
1886
     * @return string
1887
     */
1888
    public function get_name() {
1889
        // second word in class is always enrol name, sorry, no fancy plugin names with _
1890
        $words = explode('_', get_class($this));
1891
        return $words[1];
1892
    }
1893
 
1894
    /**
1895
     * Returns localised name of enrol instance
1896
     *
1441 ariadna 1897
     * @param stdClass|null $instance (null is accepted too)
1 efrain 1898
     * @return string
1899
     */
1900
    public function get_instance_name($instance) {
1901
        if (empty($instance->name)) {
1902
            $enrol = $this->get_name();
1903
            return get_string('pluginname', 'enrol_'.$enrol);
1904
        } else {
1905
            $context = context_course::instance($instance->courseid);
1906
            return format_string($instance->name, true, array('context'=>$context));
1907
        }
1908
    }
1909
 
1910
    /**
1441 ariadna 1911
     * How this enrolment method should be displayed on the "Enrolment methods" page
1912
     *
1913
     * Some plugins may choose to add more information, for example, user role, dates, etc.
1914
     *
1915
     * @param stdClass $instance
1916
     * @return string
1917
     */
1918
    public function get_instance_name_for_management_page(stdClass $instance): string {
1919
        return (string)$this->get_instance_name($instance);
1920
    }
1921
 
1922
    /**
1 efrain 1923
     * Returns optional enrolment information icons.
1924
     *
1925
     * This is used in course list for quick overview of enrolment options.
1926
     *
1927
     * We are not using single instance parameter because sometimes
1928
     * we might want to prevent icon repetition when multiple instances
1929
     * of one type exist. One instance may also produce several icons.
1930
     *
1931
     * @param array $instances all enrol instances of this type in one course
1932
     * @return array of pix_icon
1933
     */
1934
    public function get_info_icons(array $instances) {
1935
        return array();
1936
    }
1937
 
1938
    /**
1939
     * Returns optional enrolment instance description text.
1940
     *
1941
     * This is used in detailed course information.
1942
     *
1943
     *
1944
     * @param object $instance
1945
     * @return string short html text
1946
     */
1947
    public function get_description_text($instance) {
1948
        return null;
1949
    }
1950
 
1951
    /**
1952
     * Makes sure config is loaded and cached.
1953
     * @return void
1954
     */
1955
    protected function load_config() {
1956
        if (!isset($this->config)) {
1957
            $name = $this->get_name();
1958
            $this->config = get_config("enrol_$name");
1959
        }
1960
    }
1961
 
1962
    /**
1963
     * Returns plugin config value
1964
     * @param  string $name
1965
     * @param  string $default value if config does not exist yet
1966
     * @return string value or default
1967
     */
1968
    public function get_config($name, $default = NULL) {
1969
        $this->load_config();
1970
        return isset($this->config->$name) ? $this->config->$name : $default;
1971
    }
1972
 
1973
    /**
1974
     * Sets plugin config value
1975
     * @param  string $name name of config
1976
     * @param  string $value string config value, null means delete
1977
     */
1978
    public function set_config($name, $value) {
1979
        $pluginname = $this->get_name();
1980
        $this->load_config();
1981
        if ($value === NULL) {
1982
            unset($this->config->$name);
1983
        } else {
1984
            $this->config->$name = $value;
1985
        }
1986
        set_config($name, $value, "enrol_$pluginname");
1987
    }
1988
 
1989
    /**
1990
     * Does this plugin assign protected roles are can they be manually removed?
1991
     * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1992
     */
1993
    public function roles_protected() {
1994
        return true;
1995
    }
1996
 
1997
    /**
1998
     * Does this plugin allow manual enrolments?
1999
     *
2000
     * @param stdClass $instance course enrol instance
2001
     * All plugins allowing this must implement 'enrol/xxx:enrol' capability
2002
     *
2003
     * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
2004
     */
2005
    public function allow_enrol(stdClass $instance) {
2006
        return false;
2007
    }
2008
 
2009
    /**
2010
     * Does this plugin allow manual unenrolment of all users?
2011
     * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2012
     *
2013
     * @param stdClass $instance course enrol instance
2014
     * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
2015
     */
2016
    public function allow_unenrol(stdClass $instance) {
2017
        return false;
2018
    }
2019
 
2020
    /**
2021
     * Does this plugin allow manual unenrolment of a specific user?
2022
     * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
2023
     *
2024
     * This is useful especially for synchronisation plugins that
2025
     * do suspend instead of full unenrolment.
2026
     *
2027
     * @param stdClass $instance course enrol instance
2028
     * @param stdClass $ue record from user_enrolments table, specifies user
2029
     *
2030
     * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
2031
     */
2032
    public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
2033
        return $this->allow_unenrol($instance);
2034
    }
2035
 
2036
    /**
2037
     * Does this plugin allow manual changes in user_enrolments table?
2038
     *
2039
     * All plugins allowing this must implement 'enrol/xxx:manage' capability
2040
     *
2041
     * @param stdClass $instance course enrol instance
2042
     * @return bool - true means it is possible to change enrol period and status in user_enrolments table
2043
     */
2044
    public function allow_manage(stdClass $instance) {
2045
        return false;
2046
    }
2047
 
2048
    /**
2049
     * Does this plugin support some way to user to self enrol?
2050
     *
2051
     * @param stdClass $instance course enrol instance
2052
     *
2053
     * @return bool - true means show "Enrol me in this course" link in course UI
2054
     */
2055
    public function show_enrolme_link(stdClass $instance) {
2056
        return false;
2057
    }
2058
 
2059
    /**
2060
     * Does this plugin support some way to self enrol?
2061
     * This function doesn't check user capabilities. Use can_self_enrol to check capabilities.
2062
     *
2063
     * @param stdClass $instance enrolment instance
2064
     * @return bool - true means "Enrol me in this course" link could be available.
2065
     */
2066
    public function is_self_enrol_available(stdClass $instance) {
2067
        return false;
2068
    }
2069
 
2070
    /**
2071
     * Attempt to automatically enrol current user in course without any interaction,
2072
     * calling code has to make sure the plugin and instance are active.
2073
     *
2074
     * This should return either a timestamp in the future or false.
2075
     *
2076
     * @param stdClass $instance course enrol instance
2077
     * @return bool|int false means not enrolled, integer means timeend
2078
     */
2079
    public function try_autoenrol(stdClass $instance) {
2080
        global $USER;
2081
 
2082
        return false;
2083
    }
2084
 
2085
    /**
2086
     * Attempt to automatically gain temporary guest access to course,
2087
     * calling code has to make sure the plugin and instance are active.
2088
     *
2089
     * This should return either a timestamp in the future or false.
2090
     *
2091
     * @param stdClass $instance course enrol instance
2092
     * @return bool|int false means no guest access, integer means timeend
2093
     */
2094
    public function try_guestaccess(stdClass $instance) {
2095
        global $USER;
2096
 
2097
        return false;
2098
    }
2099
 
2100
    /**
2101
     * Enrol user into course via enrol instance.
2102
     *
2103
     * @param stdClass $instance
2104
     * @param int $userid
2105
     * @param int $roleid optional role id
2106
     * @param int $timestart 0 means unknown
2107
     * @param int $timeend 0 means forever
2108
     * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
2109
     * @param bool $recovergrades restore grade history
2110
     * @return void
2111
     */
2112
    public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
2113
        global $DB, $USER, $CFG; // CFG necessary!!!
2114
 
2115
        if ($instance->courseid == SITEID) {
2116
            throw new coding_exception('invalid attempt to enrol into frontpage course!');
2117
        }
2118
 
2119
        $name = $this->get_name();
2120
        $courseid = $instance->courseid;
2121
 
2122
        if ($instance->enrol !== $name) {
2123
            throw new coding_exception('invalid enrol instance!');
2124
        }
2125
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2126
        if (!isset($recovergrades)) {
2127
            $recovergrades = $CFG->recovergradesdefault;
2128
        }
2129
 
2130
        $inserted = false;
2131
        $updated  = false;
2132
        if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2133
            //only update if timestart or timeend or status are different.
2134
            if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
2135
                $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
2136
            }
2137
        } else {
2138
            $ue = new stdClass();
2139
            $ue->enrolid      = $instance->id;
2140
            $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
2141
            $ue->userid       = $userid;
2142
            $ue->timestart    = $timestart;
2143
            $ue->timeend      = $timeend;
2144
            $ue->modifierid   = $USER->id;
2145
            $ue->timecreated  = time();
2146
            $ue->timemodified = $ue->timecreated;
2147
            $ue->id = $DB->insert_record('user_enrolments', $ue);
2148
 
2149
            $inserted = true;
2150
        }
2151
 
2152
        if ($inserted) {
2153
            // Trigger event.
2154
            $event = \core\event\user_enrolment_created::create(
2155
                    array(
2156
                        'objectid' => $ue->id,
2157
                        'courseid' => $courseid,
2158
                        'context' => $context,
2159
                        'relateduserid' => $ue->userid,
2160
                        'other' => array('enrol' => $name)
2161
                        )
2162
                    );
2163
            $event->trigger();
2164
            // Check if course contacts cache needs to be cleared.
2165
            core_course_category::user_enrolment_changed($courseid, $ue->userid,
2166
                    $ue->status, $ue->timestart, $ue->timeend);
2167
        }
2168
 
2169
        // Dispatch the hook for post enrol user actions.
2170
        $hook = new \core_enrol\hook\after_user_enrolled(
2171
            enrolinstance: $instance,
2172
            userenrolmentinstance: $ue,
1441 ariadna 2173
            roleid: $roleid,
1 efrain 2174
        );
2175
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2176
 
2177
        if ($roleid) {
2178
            // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
2179
            if ($this->roles_protected()) {
2180
                role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
2181
            } else {
2182
                role_assign($roleid, $userid, $context->id);
2183
            }
2184
        }
2185
 
2186
        // Recover old grades if present.
2187
        if ($recovergrades) {
2188
            require_once("$CFG->libdir/gradelib.php");
2189
            grade_recover_history_grades($userid, $courseid);
2190
        }
2191
 
2192
        // reset current user enrolment caching
2193
        if ($userid == $USER->id) {
2194
            if (isset($USER->enrol['enrolled'][$courseid])) {
2195
                unset($USER->enrol['enrolled'][$courseid]);
2196
            }
2197
            if (isset($USER->enrol['tempguest'][$courseid])) {
2198
                unset($USER->enrol['tempguest'][$courseid]);
2199
                remove_temp_course_roles($context);
2200
            }
2201
        }
2202
    }
2203
 
2204
    /**
2205
     * Store user_enrolments changes and trigger event.
2206
     *
2207
     * @param stdClass $instance
2208
     * @param int $userid
2209
     * @param int $status
2210
     * @param int $timestart
2211
     * @param int $timeend
2212
     * @return void
2213
     */
2214
    public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
2215
        global $DB, $USER, $CFG;
2216
 
2217
        $name = $this->get_name();
2218
 
2219
        if ($instance->enrol !== $name) {
2220
            throw new coding_exception('invalid enrol instance!');
2221
        }
2222
 
2223
        if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2224
            // weird, user not enrolled
2225
            return;
2226
        }
2227
 
2228
        $modified = false;
2229
        $statusmodified = false;
2230
        $timeendmodified = false;
2231
        if (isset($status) and $ue->status != $status) {
2232
            $ue->status = $status;
2233
            $modified = true;
2234
            $statusmodified = true;
2235
        }
2236
        if (isset($timestart) and $ue->timestart != $timestart) {
2237
            $ue->timestart = $timestart;
2238
            $modified = true;
2239
        }
2240
        if (isset($timeend) and $ue->timeend != $timeend) {
2241
            $ue->timeend = $timeend;
2242
            $modified = true;
2243
            $timeendmodified = true;
2244
        }
2245
 
2246
        if (!$modified) {
2247
            // no change
2248
            return;
2249
        }
2250
 
2251
        // Dispatch the hook for pre user enrolment update actions.
2252
        $hook = new \core_enrol\hook\before_user_enrolment_updated(
2253
            enrolinstance: $instance,
2254
            userenrolmentinstance: $ue,
2255
            statusmodified: $statusmodified,
2256
            timeendmodified: $timeendmodified,
2257
        );
2258
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2259
 
2260
        $ue->modifierid = $USER->id;
2261
        $ue->timemodified = time();
2262
        $DB->update_record('user_enrolments', $ue);
2263
 
2264
        // User enrolments have changed, so mark user as dirty.
2265
        mark_user_dirty($userid);
2266
 
2267
        // Invalidate core_access cache for get_suspended_userids.
2268
        cache_helper::invalidate_by_definition('core', 'suspended_userids', array(), array($instance->courseid));
2269
 
2270
        // Trigger event.
2271
        $event = \core\event\user_enrolment_updated::create(
2272
                array(
2273
                    'objectid' => $ue->id,
2274
                    'courseid' => $instance->courseid,
2275
                    'context' => context_course::instance($instance->courseid),
2276
                    'relateduserid' => $ue->userid,
2277
                    'other' => array('enrol' => $name)
2278
                    )
2279
                );
2280
        $event->trigger();
2281
 
2282
        core_course_category::user_enrolment_changed($instance->courseid, $ue->userid,
2283
                $ue->status, $ue->timestart, $ue->timeend);
2284
    }
2285
 
2286
    /**
2287
     * Unenrol user from course,
2288
     * the last unenrolment removes all remaining roles.
2289
     *
2290
     * @param stdClass $instance
2291
     * @param int $userid
2292
     * @return void
2293
     */
2294
    public function unenrol_user(stdClass $instance, $userid) {
2295
        global $CFG, $USER, $DB;
2296
        require_once("$CFG->dirroot/group/lib.php");
2297
 
2298
        $name = $this->get_name();
2299
        $courseid = $instance->courseid;
2300
 
2301
        if ($instance->enrol !== $name) {
2302
            throw new coding_exception('invalid enrol instance!');
2303
        }
2304
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2305
 
2306
        if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
2307
            // weird, user not enrolled
2308
            return;
2309
        }
2310
 
2311
        // Dispatch the hook for pre user unenrolment actions.
2312
        $hook = new \core_enrol\hook\before_user_enrolment_removed(
2313
            enrolinstance: $instance,
2314
            userenrolmentinstance: $ue,
2315
        );
2316
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2317
 
2318
        // Remove all users groups linked to this enrolment instance.
2319
        if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
2320
            foreach ($gms as $gm) {
2321
                groups_remove_member($gm->groupid, $gm->userid);
2322
            }
2323
        }
2324
 
2325
        role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
2326
        $DB->delete_records('user_enrolments', array('id'=>$ue->id));
2327
 
2328
        // add extra info and trigger event
2329
        $ue->courseid  = $courseid;
2330
        $ue->enrol     = $name;
2331
 
2332
        $sql = "SELECT 'x'
2333
                  FROM {user_enrolments} ue
2334
                  JOIN {enrol} e ON (e.id = ue.enrolid)
2335
                 WHERE ue.userid = :userid AND e.courseid = :courseid";
2336
        if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
2337
            $ue->lastenrol = false;
2338
 
2339
        } else {
2340
            // the big cleanup IS necessary!
2341
            require_once("$CFG->libdir/gradelib.php");
2342
 
2343
            // remove all remaining roles
2344
            role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
2345
 
2346
            //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
2347
            groups_delete_group_members($courseid, $userid);
2348
 
2349
            grade_user_unenrol($courseid, $userid);
2350
 
2351
            $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
2352
 
2353
            $ue->lastenrol = true; // means user not enrolled any more
2354
        }
2355
        // Trigger event.
2356
        $event = \core\event\user_enrolment_deleted::create(
2357
                array(
2358
                    'courseid' => $courseid,
2359
                    'context' => $context,
2360
                    'relateduserid' => $ue->userid,
2361
                    'objectid' => $ue->id,
2362
                    'other' => array(
2363
                        'userenrolment' => (array)$ue,
2364
                        'enrol' => $name
2365
                        )
2366
                    )
2367
                );
2368
        $event->trigger();
2369
        // User enrolments have changed, so mark user as dirty.
2370
        mark_user_dirty($userid);
2371
 
2372
        // Check if courrse contacts cache needs to be cleared.
2373
        core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED);
2374
 
2375
        // reset current user enrolment caching
2376
        if ($userid == $USER->id) {
2377
            if (isset($USER->enrol['enrolled'][$courseid])) {
2378
                unset($USER->enrol['enrolled'][$courseid]);
2379
            }
2380
            if (isset($USER->enrol['tempguest'][$courseid])) {
2381
                unset($USER->enrol['tempguest'][$courseid]);
2382
                remove_temp_course_roles($context);
2383
            }
2384
        }
2385
    }
2386
 
2387
    /**
2388
     * Forces synchronisation of user enrolments.
2389
     *
2390
     * This is important especially for external enrol plugins,
2391
     * this function is called for all enabled enrol plugins
2392
     * right after every user login.
2393
     *
2394
     * @param object $user user record
2395
     * @return void
2396
     */
2397
    public function sync_user_enrolments($user) {
2398
        // override if necessary
2399
    }
2400
 
2401
    /**
2402
     * This returns false for backwards compatibility, but it is really recommended.
2403
     *
2404
     * @since Moodle 3.1
2405
     * @return boolean
2406
     */
2407
    public function use_standard_editing_ui() {
2408
        return false;
2409
    }
2410
 
2411
    /**
2412
     * Return whether or not, given the current state, it is possible to add a new instance
2413
     * of this enrolment plugin to the course.
2414
     *
2415
     * Default implementation is just for backwards compatibility.
2416
     *
2417
     * @param int $courseid
2418
     * @return boolean
2419
     */
2420
    public function can_add_instance($courseid) {
2421
        $link = $this->get_newinstance_link($courseid);
2422
        return !empty($link);
2423
    }
2424
 
2425
    /**
2426
     * Return whether or not, given the current state, it is possible to edit an instance
2427
     * of this enrolment plugin in the course. Used by the standard editing UI
2428
     * to generate a link to the edit instance form if editing is allowed.
2429
     *
2430
     * @param stdClass $instance
2431
     * @return boolean
2432
     */
2433
    public function can_edit_instance($instance) {
2434
        $context = context_course::instance($instance->courseid);
2435
 
2436
        return has_capability('enrol/' . $instance->enrol . ':config', $context);
2437
    }
2438
 
2439
    /**
2440
     * Returns link to page which may be used to add new instance of enrolment plugin in course.
2441
     * @param int $courseid
2442
     * @return ?moodle_url page url
2443
     */
2444
    public function get_newinstance_link($courseid) {
2445
        // override for most plugins, check if instance already exists in cases only one instance is supported
2446
        return NULL;
2447
    }
2448
 
2449
    /**
2450
     * Is it possible to delete enrol instance via standard UI?
2451
     *
2452
     * @param stdClass  $instance
2453
     * @return bool
2454
     */
2455
    public function can_delete_instance($instance) {
2456
        return false;
2457
    }
2458
 
2459
    /**
2460
     * Is it possible to hide/show enrol instance via standard UI?
2461
     *
2462
     * @param stdClass $instance
2463
     * @return bool
2464
     */
2465
    public function can_hide_show_instance($instance) {
2466
        debugging("The enrolment plugin '".$this->get_name()."' should override the function can_hide_show_instance().", DEBUG_DEVELOPER);
2467
        return true;
2468
    }
2469
 
2470
    /**
2471
     * Returns link to manual enrol UI if exists.
2472
     * Does the access control tests automatically.
2473
     *
2474
     * @param object $instance
2475
     * @return ?moodle_url
2476
     */
2477
    public function get_manual_enrol_link($instance) {
2478
        return NULL;
2479
    }
2480
 
2481
    /**
2482
     * Returns list of unenrol links for all enrol instances in course.
2483
     *
2484
     * @param stdClass $instance
2485
     * @return ?moodle_url or NULL if self unenrolment not supported
2486
     */
2487
    public function get_unenrolself_link($instance) {
2488
        global $USER, $CFG, $DB;
2489
 
2490
        $name = $this->get_name();
2491
        if ($instance->enrol !== $name) {
2492
            throw new coding_exception('invalid enrol instance!');
2493
        }
2494
 
2495
        if ($instance->courseid == SITEID) {
2496
            return NULL;
2497
        }
2498
 
2499
        if (!enrol_is_enabled($name)) {
2500
            return NULL;
2501
        }
2502
 
2503
        if ($instance->status != ENROL_INSTANCE_ENABLED) {
2504
            return NULL;
2505
        }
2506
 
2507
        if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
2508
            return NULL;
2509
        }
2510
 
2511
        $context = context_course::instance($instance->courseid, MUST_EXIST);
2512
 
2513
        if (!has_capability("enrol/$name:unenrolself", $context)) {
2514
            return NULL;
2515
        }
2516
 
2517
        if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
2518
            return NULL;
2519
        }
2520
 
2521
        return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
2522
    }
2523
 
2524
    /**
2525
     * Adds enrol instance UI to course edit form
2526
     *
2527
     * @param object $instance enrol instance or null if does not exist yet
2528
     * @param MoodleQuickForm $mform
2529
     * @param object $data
2530
     * @param object $context context of existing course or parent category if course does not exist
2531
     * @return void
2532
     */
2533
    public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
2534
        // override - usually at least enable/disable switch, has to add own form header
2535
    }
2536
 
2537
    /**
2538
     * Adds form elements to add/edit instance form.
2539
     *
2540
     * @since Moodle 3.1
2541
     * @param object $instance enrol instance or null if does not exist yet
2542
     * @param MoodleQuickForm $mform
2543
     * @param context $context
2544
     * @return void
2545
     */
2546
    public function edit_instance_form($instance, MoodleQuickForm $mform, $context) {
2547
        // Do nothing by default.
2548
    }
2549
 
2550
    /**
2551
     * Perform custom validation of the data used to edit the instance.
2552
     *
2553
     * @since Moodle 3.1
2554
     * @param array $data array of ("fieldname"=>value) of submitted data
2555
     * @param array $files array of uploaded files "element_name"=>tmp_file_path
2556
     * @param object $instance The instance data loaded from the DB.
2557
     * @param context $context The context of the instance we are editing
2558
     * @return array of "element_name"=>"error_description" if there are errors,
2559
     *         or an empty array if everything is OK.
2560
     */
2561
    public function edit_instance_validation($data, $files, $instance, $context) {
2562
        // No errors by default.
2563
        debugging('enrol_plugin::edit_instance_validation() is missing. This plugin has no validation!', DEBUG_DEVELOPER);
2564
        return array();
2565
    }
2566
 
2567
    /**
2568
     * Validates course edit form data
2569
     *
2570
     * @param object $instance enrol instance or null if does not exist yet
2571
     * @param array $data
2572
     * @param object $context context of existing course or parent category if course does not exist
2573
     * @return array errors array
2574
     */
2575
    public function course_edit_validation($instance, array $data, $context) {
2576
        return array();
2577
    }
2578
 
2579
    /**
2580
     * Called after updating/inserting course.
2581
     *
2582
     * @param bool $inserted true if course just inserted
2583
     * @param object $course
2584
     * @param object $data form data
2585
     * @return void
2586
     */
2587
    public function course_updated($inserted, $course, $data) {
2588
        if ($inserted) {
2589
            if ($this->get_config('defaultenrol')) {
2590
                $this->add_default_instance($course);
2591
            }
2592
        }
2593
    }
2594
 
2595
    /**
2596
     * Add new instance of enrol plugin.
2597
     * @param object $course
2598
     * @param array instance fields
2599
     * @return int id of new instance, null if can not be created
2600
     */
1441 ariadna 2601
    public function add_instance($course, ?array $fields = NULL) {
1 efrain 2602
        global $DB;
2603
 
2604
        if ($course->id == SITEID) {
2605
            throw new coding_exception('Invalid request to add enrol instance to frontpage.');
2606
        }
2607
 
2608
        $instance = new stdClass();
2609
        $instance->enrol          = $this->get_name();
2610
        $instance->status         = ENROL_INSTANCE_ENABLED;
2611
        $instance->courseid       = $course->id;
2612
        $instance->enrolstartdate = 0;
2613
        $instance->enrolenddate   = 0;
2614
        $instance->timemodified   = time();
2615
        $instance->timecreated    = $instance->timemodified;
2616
        $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
2617
 
2618
        $fields = (array)$fields;
2619
        unset($fields['enrol']);
2620
        unset($fields['courseid']);
2621
        unset($fields['sortorder']);
2622
        foreach($fields as $field=>$value) {
2623
            $instance->$field = $value;
2624
        }
2625
 
2626
        $instance->id = $DB->insert_record('enrol', $instance);
2627
 
2628
        \core\event\enrol_instance_created::create_from_record($instance)->trigger();
2629
 
2630
        return $instance->id;
2631
    }
2632
 
2633
    /**
2634
     * Update instance of enrol plugin.
2635
     *
2636
     * @since Moodle 3.1
2637
     * @param stdClass $instance
2638
     * @param stdClass $data modified instance fields
2639
     * @return boolean
2640
     */
2641
    public function update_instance($instance, $data) {
2642
        global $DB;
2643
        $properties = array('status', 'name', 'password', 'customint1', 'customint2', 'customint3',
2644
                            'customint4', 'customint5', 'customint6', 'customint7', 'customint8',
2645
                            'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2',
2646
                            'customtext1', 'customtext2', 'customtext3', 'customtext4', 'roleid',
2647
                            'enrolperiod', 'expirynotify', 'notifyall', 'expirythreshold',
2648
                            'enrolstartdate', 'enrolenddate', 'cost', 'currency');
2649
 
2650
        foreach ($properties as $key) {
2651
            if (isset($data->$key)) {
2652
                $instance->$key = $data->$key;
2653
            }
2654
        }
2655
        $instance->timemodified = time();
2656
 
2657
        $update = $DB->update_record('enrol', $instance);
2658
        if ($update) {
2659
            \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2660
        }
2661
        return $update;
2662
    }
2663
 
2664
    /**
2665
     * Add new instance of enrol plugin with default settings,
2666
     * called when adding new instance manually or when adding new course.
2667
     *
2668
     * Not all plugins support this.
2669
     *
2670
     * @param object $course
2671
     * @return ?int id of new instance or null if no default supported
2672
     */
2673
    public function add_default_instance($course) {
2674
        return null;
2675
    }
2676
 
2677
    /**
2678
     * Add new instance of enrol plugin with custom settings,
2679
     * called when adding new instance manually or when adding new course.
2680
     * Used for example on course upload.
2681
     *
2682
     * Not all plugins support this.
2683
     *
2684
     * @param stdClass $course Course object
2685
     * @param array|null $fields instance fields
2686
     * @return int|null id of new instance or null if not supported
2687
     */
2688
    public function add_custom_instance(stdClass $course, ?array $fields = null): ?int {
2689
        return null;
2690
    }
2691
 
2692
    /**
2693
     * Check if enrolment plugin is supported in csv course upload.
2694
     *
2695
     * If supported, plugins are also encouraged to override methods:
2696
     * {@see self::fill_enrol_custom_fields()}, {@see self::validate_plugin_data_context()}
2697
     *
2698
     * @return bool
2699
     */
2700
    public function is_csv_upload_supported(): bool {
2701
        return false;
2702
    }
2703
 
2704
    /**
2705
     * Update instance status
2706
     *
2707
     * Override when plugin needs to do some action when enabled or disabled.
2708
     *
2709
     * @param stdClass $instance
2710
     * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
2711
     * @return void
2712
     */
2713
    public function update_status($instance, $newstatus) {
2714
        global $DB;
2715
 
2716
        $instance->status = $newstatus;
2717
        $DB->update_record('enrol', $instance);
2718
 
2719
        // Dispatch the hook for post enrol status update actions.
2720
        $hook = new \core_enrol\hook\after_enrol_instance_status_updated(
2721
            enrolinstance: $instance,
2722
            newstatus: $newstatus,
2723
        );
2724
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2725
 
2726
        $context = context_course::instance($instance->courseid);
2727
        \core\event\enrol_instance_updated::create_from_record($instance)->trigger();
2728
 
2729
        // Invalidate all enrol caches.
2730
        $context->mark_dirty();
2731
    }
2732
 
2733
    /**
2734
     * @deprecated Since Moodle 4.4.0.
2735
     */
1441 ariadna 2736
    #[\core\attribute\deprecated(null, reason: 'Replaced with hooks', since: '4.4', mdl: 'MDL-78551', final: true)]
2737
    public function update_communication(): void {
2738
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 2739
    }
2740
 
2741
    /**
2742
     * Delete course enrol plugin instance, unenrol all users.
2743
     * @param object $instance
2744
     * @return void
2745
     */
2746
    public function delete_instance($instance) {
2747
        global $DB;
2748
 
2749
        $name = $this->get_name();
2750
        if ($instance->enrol !== $name) {
2751
            throw new coding_exception('invalid enrol instance!');
2752
        }
2753
 
2754
        // Dispatch the hook for pre enrol instance delete actions.
2755
        $hook = new \core_enrol\hook\before_enrol_instance_deleted(
2756
            enrolinstance: $instance,
2757
        );
2758
        \core\di::get(\core\hook\manager::class)->dispatch($hook);
2759
 
2760
        //first unenrol all users
2761
        $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
2762
        foreach ($participants as $participant) {
2763
            $this->unenrol_user($instance, $participant->userid);
2764
        }
2765
        $participants->close();
2766
 
2767
        // now clean up all remainders that were not removed correctly
2768
        if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
2769
            foreach ($gms as $gm) {
2770
                groups_remove_member($gm->groupid, $gm->userid);
2771
            }
2772
        }
2773
        $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
2774
        $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
2775
 
2776
        // finally drop the enrol row
2777
        $DB->delete_records('enrol', array('id'=>$instance->id));
2778
 
2779
        $context = context_course::instance($instance->courseid);
2780
        \core\event\enrol_instance_deleted::create_from_record($instance)->trigger();
2781
 
2782
        // Invalidate all enrol caches.
2783
        $context->mark_dirty();
2784
    }
2785
 
2786
    /**
1441 ariadna 2787
     * Creates a widget to display on the course enrolment page. It can also redirect.
1 efrain 2788
     *
1441 ariadna 2789
     * It is recommended that all plugins use the same template for the consistent output. Example:
2790
     *
2791
     *     $obj = new \core_enrol\output\enrol_page($instance, ...);
2792
     *     return $OUTPUT->render($obj);
2793
     *
1 efrain 2794
     * @param stdClass $instance
1441 ariadna 2795
     * @return string|null html to display on the enrolment page
1 efrain 2796
     */
2797
    public function enrol_page_hook(stdClass $instance) {
2798
        return null;
2799
    }
2800
 
2801
    /**
2802
     * Checks if user can self enrol.
2803
     *
2804
     * @param stdClass $instance enrolment instance
2805
     * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
2806
     *             used by navigation to improve performance.
2807
     * @return bool|string true if successful, else error message or false
2808
     */
2809
    public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
2810
        return false;
2811
    }
2812
 
2813
    /**
2814
     * Return information for enrolment instance containing list of parameters required
2815
     * for enrolment, name of enrolment plugin etc.
2816
     *
2817
     * @param stdClass $instance enrolment instance
2818
     * @return stdClass|null instance info.
2819
     */
2820
    public function get_enrol_info(stdClass $instance) {
2821
        return null;
2822
    }
2823
 
2824
    /**
2825
     * Adds navigation links into course admin block.
2826
     *
2827
     * By defaults looks for manage links only.
2828
     *
2829
     * @param navigation_node $instancesnode
2830
     * @param stdClass $instance
2831
     * @return void
2832
     */
2833
    public function add_course_navigation($instancesnode, stdClass $instance) {
2834
        if ($this->use_standard_editing_ui()) {
2835
            $context = context_course::instance($instance->courseid);
2836
            $cap = 'enrol/' . $instance->enrol . ':config';
2837
            if (has_capability($cap, $context)) {
2838
                $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2839
                $managelink = new moodle_url('/enrol/editinstance.php', $linkparams);
2840
                $instancesnode->add($this->get_instance_name($instance), $managelink, navigation_node::TYPE_SETTING);
2841
            }
2842
        }
2843
    }
2844
 
2845
    /**
2846
     * Returns edit icons for the page with list of instances
2847
     * @param stdClass $instance
2848
     * @return array
2849
     */
2850
    public function get_action_icons(stdClass $instance) {
2851
        global $OUTPUT;
2852
 
2853
        $icons = array();
2854
        if ($this->use_standard_editing_ui()) {
2855
            $context = context_course::instance($instance->courseid);
2856
            $cap = 'enrol/' . $instance->enrol . ':config';
2857
            if (has_capability($cap, $context)) {
2858
                $linkparams = array('courseid' => $instance->courseid, 'id' => $instance->id, 'type' => $instance->enrol);
2859
                $editlink = new moodle_url("/enrol/editinstance.php", $linkparams);
2860
                $icons[] = $OUTPUT->action_icon($editlink, new pix_icon('t/edit', get_string('edit'), 'core',
2861
                    array('class' => 'iconsmall')));
2862
            }
2863
        }
2864
        return $icons;
2865
    }
2866
 
2867
    /**
2868
     * Reads version.php and determines if it is necessary
2869
     * to execute the cron job now.
2870
     * @return bool
2871
     */
2872
    public function is_cron_required() {
2873
        global $CFG;
2874
 
2875
        $name = $this->get_name();
2876
        $versionfile = "$CFG->dirroot/enrol/$name/version.php";
2877
        $plugin = new stdClass();
2878
        include($versionfile);
2879
        if (empty($plugin->cron)) {
2880
            return false;
2881
        }
2882
        $lastexecuted = $this->get_config('lastcron', 0);
2883
        if ($lastexecuted + $plugin->cron < time()) {
2884
            return true;
2885
        } else {
2886
            return false;
2887
        }
2888
    }
2889
 
2890
    /**
2891
     * Called for all enabled enrol plugins that returned true from is_cron_required().
2892
     * @return void
2893
     */
2894
    public function cron() {
2895
    }
2896
 
2897
    /**
2898
     * Called when user is about to be deleted
2899
     * @param object $user
2900
     * @return void
2901
     */
2902
    public function user_delete($user) {
2903
        global $DB;
2904
 
2905
        $sql = "SELECT e.*
2906
                  FROM {enrol} e
2907
                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
2908
                 WHERE e.enrol = :name AND ue.userid = :userid";
2909
        $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
2910
 
2911
        $rs = $DB->get_recordset_sql($sql, $params);
2912
        foreach($rs as $instance) {
2913
            $this->unenrol_user($instance, $user->id);
2914
        }
2915
        $rs->close();
2916
    }
2917
 
2918
    /**
2919
     * Returns an enrol_user_button that takes the user to a page where they are able to
2920
     * enrol users into the managers course through this plugin.
2921
     *
2922
     * Optional: If the plugin supports manual enrolments it can choose to override this
2923
     * otherwise it shouldn't
2924
     *
2925
     * @param course_enrolment_manager $manager
2926
     * @return enrol_user_button|false
2927
     */
2928
    public function get_manual_enrol_button(course_enrolment_manager $manager) {
2929
        return false;
2930
    }
2931
 
2932
    /**
2933
     * Gets an array of the user enrolment actions
2934
     *
2935
     * @param course_enrolment_manager $manager
2936
     * @param stdClass $ue
2937
     * @return array An array of user_enrolment_actions
2938
     */
2939
    public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
2940
        $actions = [];
2941
        $context = $manager->get_context();
2942
        $instance = $ue->enrolmentinstance;
2943
        $params = $manager->get_moodlepage()->url->params();
2944
        $params['ue'] = $ue->id;
2945
 
2946
        // Edit enrolment action.
2947
        if ($this->allow_manage($instance) && has_capability("enrol/{$instance->enrol}:manage", $context)) {
2948
            $title = get_string('editenrolment', 'enrol');
2949
            $icon = new pix_icon('t/edit', $title);
2950
            $url = new moodle_url('/enrol/editenrolment.php', $params);
2951
            $actionparams = [
2952
                'class' => 'editenrollink',
2953
                'rel' => $ue->id,
2954
                'data-action' => ENROL_ACTION_EDIT
2955
            ];
2956
            $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2957
        }
2958
 
2959
        // Unenrol action.
2960
        if ($this->allow_unenrol_user($instance, $ue) && has_capability("enrol/{$instance->enrol}:unenrol", $context)) {
2961
            $title = get_string('unenrol', 'enrol');
2962
            $icon = new pix_icon('t/delete', $title);
2963
            $url = new moodle_url('/enrol/unenroluser.php', $params);
2964
            $actionparams = [
2965
                'class' => 'unenrollink',
2966
                'rel' => $ue->id,
2967
                'data-action' => ENROL_ACTION_UNENROL
2968
            ];
2969
            $actions[] = new user_enrolment_action($icon, $title, $url, $actionparams);
2970
        }
2971
        return $actions;
2972
    }
2973
 
2974
    /**
2975
     * Returns true if the plugin has one or more bulk operations that can be performed on
2976
     * user enrolments.
2977
     *
2978
     * @param course_enrolment_manager $manager
2979
     * @return bool
2980
     */
2981
    public function has_bulk_operations(course_enrolment_manager $manager) {
2982
       return false;
2983
    }
2984
 
2985
    /**
2986
     * Return an array of enrol_bulk_enrolment_operation objects that define
2987
     * the bulk actions that can be performed on user enrolments by the plugin.
2988
     *
2989
     * @param course_enrolment_manager $manager
2990
     * @return array
2991
     */
2992
    public function get_bulk_operations(course_enrolment_manager $manager) {
2993
        return array();
2994
    }
2995
 
2996
    /**
2997
     * Do any enrolments need expiration processing.
2998
     *
2999
     * Plugins that want to call this functionality must implement 'expiredaction' config setting.
3000
     *
3001
     * @param progress_trace $trace
3002
     * @param int $courseid one course, empty mean all
3003
     * @return bool true if any data processed, false if not
3004
     */
3005
    public function process_expirations(progress_trace $trace, $courseid = null) {
3006
        global $DB;
3007
 
3008
        $name = $this->get_name();
3009
        if (!enrol_is_enabled($name)) {
3010
            $trace->finished();
3011
            return false;
3012
        }
3013
 
3014
        $processed = false;
3015
        $params = array();
3016
        $coursesql = "";
3017
        if ($courseid) {
3018
            $coursesql = "AND e.courseid = :courseid";
3019
        }
3020
 
3021
        // Deal with expired accounts.
3022
        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
3023
 
3024
        if ($action == ENROL_EXT_REMOVED_UNENROL) {
3025
            $instances = array();
3026
            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3027
                      FROM {user_enrolments} ue
3028
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3029
                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3030
                     WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
3031
            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
3032
 
3033
            $rs = $DB->get_recordset_sql($sql, $params);
3034
            foreach ($rs as $ue) {
3035
                if (!$processed) {
3036
                    $trace->output("Starting processing of enrol_$name expirations...");
3037
                    $processed = true;
3038
                }
3039
                if (empty($instances[$ue->enrolid])) {
3040
                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3041
                }
3042
                $instance = $instances[$ue->enrolid];
3043
                if (!$this->roles_protected()) {
3044
                    // Let's just guess what extra roles are supposed to be removed.
3045
                    if ($instance->roleid) {
3046
                        role_unassign($instance->roleid, $ue->userid, $ue->contextid);
3047
                    }
3048
                }
3049
                // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
3050
                $this->unenrol_user($instance, $ue->userid);
3051
                $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
3052
            }
3053
            $rs->close();
3054
            unset($instances);
3055
 
3056
        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
3057
            $instances = array();
3058
            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
3059
                      FROM {user_enrolments} ue
3060
                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
3061
                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
3062
                     WHERE ue.timeend > 0 AND ue.timeend < :now
3063
                           AND ue.status = :useractive $coursesql";
3064
            $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
3065
            $rs = $DB->get_recordset_sql($sql, $params);
3066
            foreach ($rs as $ue) {
3067
                if (!$processed) {
3068
                    $trace->output("Starting processing of enrol_$name expirations...");
3069
                    $processed = true;
3070
                }
3071
                if (empty($instances[$ue->enrolid])) {
3072
                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
3073
                }
3074
                $instance = $instances[$ue->enrolid];
3075
 
3076
                if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
3077
                    if (!$this->roles_protected()) {
3078
                        // Let's just guess what roles should be removed.
3079
                        $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
3080
                        if ($count == 1) {
3081
                            role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
3082
 
3083
                        } else if ($count > 1 and $instance->roleid) {
3084
                            role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
3085
                        }
3086
                    }
3087
                    // In any case remove all roles that belong to this instance and user.
3088
                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
3089
                    // Final cleanup of subcontexts if there are no more course roles.
3090
                    if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
3091
                        role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
3092
                    }
3093
                }
3094
 
3095
                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
3096
                $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
3097
            }
3098
            $rs->close();
3099
            unset($instances);
3100
 
3101
        } else {
3102
            // ENROL_EXT_REMOVED_KEEP means no changes.
3103
        }
3104
 
3105
        if ($processed) {
3106
            $trace->output("...finished processing of enrol_$name expirations");
3107
        } else {
3108
            $trace->output("No expired enrol_$name enrolments detected");
3109
        }
3110
        $trace->finished();
3111
 
3112
        return $processed;
3113
    }
3114
 
3115
    /**
3116
     * Send expiry notifications.
3117
     *
3118
     * Plugin that wants to have expiry notification MUST implement following:
3119
     * - expirynotifyhour plugin setting,
3120
     * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
3121
     * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
3122
     *   expirymessageenrolledsubject and expirymessageenrolledbody),
3123
     * - expiry_notification provider in db/messages.php,
3124
     * - upgrade code that sets default thresholds for existing courses (should be 1 day),
3125
     * - something that calls this method, such as cron.
3126
     *
3127
     * @param progress_trace $trace (accepts bool for backwards compatibility only)
3128
     */
3129
    public function send_expiry_notifications($trace) {
3130
        global $CFG;
3131
 
3132
        $name = $this->get_name();
3133
        if (!enrol_is_enabled($name)) {
3134
            $trace->finished();
3135
            return;
3136
        }
3137
 
3138
        // Unfortunately this may take a long time, it should not be interrupted,
3139
        // otherwise users get duplicate notification.
3140
 
3141
        core_php_time_limit::raise();
3142
        raise_memory_limit(MEMORY_HUGE);
3143
 
3144
 
3145
        $expirynotifylast = $this->get_config('expirynotifylast', 0);
3146
        $expirynotifyhour = $this->get_config('expirynotifyhour');
3147
        if (is_null($expirynotifyhour)) {
3148
            debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
3149
            $trace->finished();
3150
            return;
3151
        }
3152
 
3153
        if (!($trace instanceof progress_trace)) {
3154
            $trace = $trace ? new text_progress_trace() : new null_progress_trace();
3155
            debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
3156
        }
3157
 
3158
        $timenow = time();
3159
        $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
3160
 
3161
        if ($expirynotifylast > $notifytime) {
3162
            $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
3163
            $trace->finished();
3164
            return;
3165
 
3166
        } else if ($timenow < $notifytime) {
3167
            $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
3168
            $trace->finished();
3169
            return;
3170
        }
3171
 
3172
        $trace->output('Processing '.$name.' enrolment expiration notifications...');
3173
 
3174
        // Notify users responsible for enrolment once every day.
3175
        $this->fetch_users_and_notify_expiry($timenow, $name, $trace);
3176
 
3177
        $trace->output('...notification processing finished.');
3178
        $trace->finished();
3179
 
3180
        $this->set_config('expirynotifylast', $timenow);
3181
    }
3182
 
3183
    /**
3184
     * Notify users about enrolment expiration.
3185
     *
3186
     * Retrieves enrolment data from the database and notifies users about their
3187
     * upcoming course enrolment expiration based on expiry thresholds and notification settings.
3188
     *
3189
     * @param int $timenow Current time.
3190
     * @param string $name Name of this enrol plugin.
3191
     * @param progress_trace $trace (accepts bool for backwards compatibility only).
3192
     * @return void
3193
     */
3194
    protected function fetch_users_and_notify_expiry(int $timenow, string $name, progress_trace $trace): void {
3195
        global $DB, $CFG;
3196
 
3197
        $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
3198
                  FROM {user_enrolments} ue
3199
                  JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
3200
                  JOIN {course} c ON (c.id = e.courseid)
3201
                  JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
3202
                 WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
3203
              ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
3204
        $params = [
3205
            'enabled' => ENROL_INSTANCE_ENABLED,
3206
            'active' => ENROL_USER_ACTIVE,
3207
            'now1' => $timenow,
3208
            'now2' => $timenow,
3209
            'name' => $name,
3210
        ];
3211
 
3212
        $rs = $DB->get_recordset_sql($sql, $params);
3213
 
3214
        $lastenrollid = 0;
3215
        $users = [];
3216
 
3217
        foreach ($rs as $ue) {
3218
            if ($lastenrollid && $lastenrollid != $ue->enrolid) {
3219
                $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3220
                $users = [];
3221
            }
3222
            $lastenrollid = $ue->enrolid;
3223
 
3224
            $enroller = $this->get_enroller($ue->enrolid);
3225
            $context = context_course::instance($ue->courseid);
3226
 
3227
            $user = $DB->get_record('user', ['id' => $ue->userid]);
3228
 
3229
            $users[] = [
3230
                'fullname' => fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)),
3231
                'timeend' => $ue->timeend,
3232
            ];
3233
 
3234
            if (!$ue->notifyall) {
3235
                continue;
3236
            }
3237
 
3238
            if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
3239
                // Notify enrolled users only once at the start of the threshold.
3240
                $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".
3241
                    userdate($ue->timeend, '', $CFG->timezone), 1);
3242
                continue;
3243
            }
3244
 
3245
            $this->notify_expiry_enrolled($user, $ue, $trace);
3246
        }
3247
        $rs->close();
3248
 
3249
        if ($lastenrollid && $users) {
3250
            $this->notify_expiry_enroller($lastenrollid, $users, $trace);
3251
        }
3252
    }
3253
 
3254
    /**
3255
     * Returns the user who is responsible for enrolments for given instance.
3256
     *
3257
     * Override if plugin knows anybody better than admin.
3258
     *
3259
     * @param int $instanceid enrolment instance id
3260
     * @return stdClass user record
3261
     */
3262
    protected function get_enroller($instanceid) {
3263
        return get_admin();
3264
    }
3265
 
3266
    /**
3267
     * Notify user about incoming expiration of their enrolment,
3268
     * it is called only if notification of enrolled users (aka students) is enabled in course.
3269
     *
3270
     * This is executed only once for each expiring enrolment right
3271
     * at the start of the expiration threshold.
3272
     *
3273
     * @param stdClass $user
3274
     * @param stdClass $ue
3275
     * @param progress_trace $trace
3276
     */
3277
    protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
3278
        global $CFG;
3279
 
3280
        $name = $this->get_name();
3281
 
3282
        $oldforcelang = force_current_language($user->lang);
3283
 
3284
        $enroller = $this->get_enroller($ue->enrolid);
3285
        $context = context_course::instance($ue->courseid);
3286
 
3287
        $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name);
3288
        $body = $this->get_expiry_message_body($user, $ue, $name, $enroller, $context);
3289
 
3290
        $coursename = format_string($ue->fullname, true, ['context' => $context]);
3291
 
3292
        $message = new \core\message\message();
3293
        $message->courseid          = $ue->courseid;
3294
        $message->notification      = 1;
3295
        $message->component         = 'enrol_'.$name;
3296
        $message->name              = 'expiry_notification';
3297
        $message->userfrom          = $enroller;
3298
        $message->userto            = $user;
3299
        $message->subject           = $subject;
3300
        $message->fullmessage       = $body;
3301
        $message->fullmessageformat = FORMAT_MARKDOWN;
3302
        $message->fullmessagehtml   = markdown_to_html($body);
3303
        $message->smallmessage      = $subject;
3304
        $message->contexturlname    = $coursename;
3305
        $message->contexturl        = (string)new moodle_url('/course/view.php', ['id' => $ue->courseid]);
3306
 
3307
        if (message_send($message)) {
3308
            $stringmessage = 'notifying user %s that enrolment in course %s expires on %s';
3309
        } else {
3310
            $stringmessage = 'error notifying user %s that enrolment in course %s expires on %s';
3311
        }
3312
        $outputmessage = sprintf($stringmessage, $ue->userid, $ue->courseid, userdate($ue->timeend, '', $CFG->timezone));
3313
        $trace->output($outputmessage, 1);
3314
 
3315
        force_current_language($oldforcelang);
3316
    }
3317
 
3318
    /**
3319
     * Generate subject and body messages for enrolment expiration notification.
3320
     *
3321
     * @param stdClass $user An object representing the user.
3322
     * @param stdClass $ue An object containing enrolment data.
3323
     * @param string $name Name of this enrol plugin.
3324
     * @param stdClass $enroller The user who is responsible for enrolments.
3325
     * @param context $context The context object.
3326
     * @return string Return the body message.
3327
     */
3328
    protected function get_expiry_message_body(stdClass $user, stdClass $ue, string $name,
3329
            stdClass $enroller, context $context): string {
3330
        $a = new stdClass();
3331
        $a->course   = format_string($ue->fullname, true, ['context' => $context]);
3332
        $a->user     = fullname($user, true);
3333
        $a->timeend  = userdate($ue->timeend, '', $user->timezone);
3334
        $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
3335
        return get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
3336
    }
3337
 
3338
    /**
3339
     * Notify person responsible for enrolments that some user enrolments will be expired soon,
3340
     * it is called only if notification of enrollers (aka teachers) is enabled in course.
3341
     *
3342
     * This is called repeatedly every day for each course if there are any pending expiration
3343
     * in the expiration threshold.
3344
     *
3345
     * @param int $eid
3346
     * @param array $users
3347
     * @param progress_trace $trace
3348
     */
3349
    protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
3350
        global $DB;
3351
 
3352
        $name = $this->get_name();
3353
 
3354
        $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
3355
        $context = context_course::instance($instance->courseid);
3356
        $course = $DB->get_record('course', array('id'=>$instance->courseid));
3357
 
3358
        $enroller = $this->get_enroller($instance->id);
3359
        $admin = get_admin();
3360
 
3361
        $oldforcelang = force_current_language($enroller->lang);
3362
 
3363
        foreach($users as $key=>$info) {
3364
            $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
3365
        }
3366
 
3367
        $a = new stdClass();
3368
        $a->course    = format_string($course->fullname, true, array('context'=>$context));
3369
        $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
3370
        $a->users     = implode("\n", $users);
3371
        $a->extendurl = (string)new moodle_url('/user/index.php', array('id'=>$instance->courseid));
3372
 
3373
        $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
3374
        $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
3375
 
3376
        $message = new \core\message\message();
3377
        $message->courseid          = $course->id;
3378
        $message->notification      = 1;
3379
        $message->component         = 'enrol_'.$name;
3380
        $message->name              = 'expiry_notification';
3381
        $message->userfrom          = $admin;
3382
        $message->userto            = $enroller;
3383
        $message->subject           = $subject;
3384
        $message->fullmessage       = $body;
3385
        $message->fullmessageformat = FORMAT_MARKDOWN;
3386
        $message->fullmessagehtml   = markdown_to_html($body);
3387
        $message->smallmessage      = $subject;
3388
        $message->contexturlname    = $a->course;
3389
        $message->contexturl        = $a->extendurl;
3390
 
3391
        if (message_send($message)) {
3392
            $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3393
        } else {
3394
            $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
3395
        }
3396
 
3397
        force_current_language($oldforcelang);
3398
    }
3399
 
3400
    /**
3401
     * Backup execution step hook to annotate custom fields.
3402
     *
3403
     * @param backup_enrolments_execution_step $step
3404
     * @param stdClass $enrol
3405
     */
3406
    public function backup_annotate_custom_fields(backup_enrolments_execution_step $step, stdClass $enrol) {
3407
        // Override as necessary to annotate custom fields in the enrol table.
3408
    }
3409
 
3410
    /**
3411
     * Automatic enrol sync executed during restore.
3412
     * Useful for automatic sync by course->idnumber or course category.
3413
     * @param stdClass $course course record
3414
     */
3415
    public function restore_sync_course($course) {
3416
        // Override if necessary.
3417
    }
3418
 
3419
    /**
3420
     * Restore instance and map settings.
3421
     *
3422
     * @param restore_enrolments_structure_step $step
3423
     * @param stdClass $data
3424
     * @param stdClass $course
3425
     * @param int $oldid
3426
     */
3427
    public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
3428
        // Do not call this from overridden methods, restore and set new id there.
3429
        $step->set_mapping('enrol', $oldid, 0);
3430
    }
3431
 
3432
    /**
3433
     * Restore user enrolment.
3434
     *
3435
     * @param restore_enrolments_structure_step $step
3436
     * @param stdClass $data
3437
     * @param stdClass $instance
3438
     * @param int $oldinstancestatus
3439
     * @param int $userid
3440
     */
3441
    public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
3442
        // Override as necessary if plugin supports restore of enrolments.
3443
    }
3444
 
3445
    /**
3446
     * Restore role assignment.
3447
     *
3448
     * @param stdClass $instance
3449
     * @param int $roleid
3450
     * @param int $userid
3451
     * @param int $contextid
3452
     */
3453
    public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
3454
        // No role assignment by default, override if necessary.
3455
    }
3456
 
3457
    /**
3458
     * Restore user group membership.
3459
     * @param stdClass $instance
3460
     * @param int $groupid
3461
     * @param int $userid
3462
     */
3463
    public function restore_group_member($instance, $groupid, $userid) {
3464
        // Implement if you want to restore protected group memberships,
3465
        // usually this is not necessary because plugins should be able to recreate the memberships automatically.
3466
    }
3467
 
3468
    /**
3469
     * Returns defaults for new instances.
3470
     * @since Moodle 3.1
3471
     * @return array
3472
     */
3473
    public function get_instance_defaults() {
3474
        return array();
3475
    }
3476
 
3477
    /**
3478
     * Validate a list of parameter names and types.
3479
     * @since Moodle 3.1
3480
     *
3481
     * @param array $data array of ("fieldname"=>value) of submitted data
3482
     * @param array $rules array of ("fieldname"=>PARAM_X types - or "fieldname"=>array( list of valid options )
3483
     * @return array of "element_name"=>"error_description" if there are errors,
3484
     *         or an empty array if everything is OK.
3485
     */
3486
    public function validate_param_types($data, $rules) {
3487
        $errors = array();
3488
        $invalidstr = get_string('invaliddata', 'error');
3489
        foreach ($rules as $fieldname => $rule) {
3490
            if (!array_key_exists($fieldname, $data)) {
3491
                continue;
3492
            }
3493
            if (is_array($rule)) {
3494
                if (!in_array($data[$fieldname], $rule)) {
3495
                    $errors[$fieldname] = $invalidstr;
3496
                }
3497
            } else {
3498
                if ($data[$fieldname] != clean_param($data[$fieldname], $rule)) {
3499
                    $errors[$fieldname] = $invalidstr;
3500
                }
3501
            }
3502
        }
3503
        return $errors;
3504
    }
3505
 
3506
    /**
3507
     * Fill custom fields data for a given enrolment plugin.
3508
     *
3509
     * For example: resolve linked entities from the idnumbers (cohort, role, group, etc.)
3510
     * Also fill the default values that are not specified.
3511
     *
3512
     * @param array $enrolmentdata enrolment data received in CSV file in tool_uploadcourse
3513
     * @param int $courseid Course ID.
3514
     * @return array Updated enrolment data with custom fields info.
3515
     */
3516
    public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid): array {
3517
        return $enrolmentdata;
3518
    }
3519
 
3520
    /**
3521
     * Check if data is valid for a given enrolment plugin
3522
     *
3523
     * @param array $enrolmentdata enrolment data to validate.
3524
     * @param int|null $courseid Course ID.
3525
     * @return array Errors
3526
     */
3527
    public function validate_enrol_plugin_data(array $enrolmentdata, ?int $courseid = null): array {
3528
        $errors = [];
3529
        if (!$this->is_csv_upload_supported()) {
3530
            $errors['errorunsupportedmethod'] =
3531
                new lang_string('errorunsupportedmethod', 'tool_uploadcourse',
3532
                    get_class($this));
3533
        } else {
3534
            $plugin = $this->get_name();
3535
            if (!enrol_is_enabled($plugin)) {
3536
                $pluginname = get_string('pluginname', 'enrol_' . $plugin);
3537
                $errors['plugindisabled'] = new lang_string('plugindisabled', 'enrol', $pluginname);
3538
            }
3539
        }
3540
        return $errors;
3541
    }
3542
 
3543
    /**
3544
     * Check if plugin custom data is allowed in relevant context.
3545
     *
3546
     * This is called from the tool_uploadcourse if the plugin supports instance creation in
3547
     * upload course ({@see self::is_csv_upload_supported()})
3548
     *
3549
     * Override it if plugin can validate provided data in relevant context.
3550
     *
3551
     * @param array $enrolmentdata enrolment data to validate.
3552
     * @param int|null $courseid Course ID.
3553
     * @return lang_string|null Error
3554
     */
3555
    public function validate_plugin_data_context(array $enrolmentdata, ?int $courseid = null): ?lang_string {
3556
        return null;
3557
    }
3558
 
3559
    /**
3560
     * Finds matching instances for a given course.
3561
     *
3562
     * @param array $enrolmentdata enrolment data.
3563
     * @param int $courseid Course ID.
3564
     * @return stdClass|null Matching instance
3565
     */
3566
    public function find_instance(array $enrolmentdata, int $courseid): ?stdClass {
3567
 
3568
        // By default, we assume we can't uniquely identify an instance so better not update any.
3569
        // Plugins can override this if they can uniquely identify an instance.
3570
        return null;
3571
    }
3572
 
3573
    /**
3574
     * Get the "from" contact which the message will be sent from.
3575
     *
3576
     * @param int $sendoption send email from constant ENROL_SEND_EMAIL_FROM_*
3577
     * @param context $context where the user will be fetched from.
3578
     * @return null|stdClass the contact user object.
3579
     */
3580
    public function get_welcome_message_contact(
3581
        int $sendoption,
3582
        context $context,
3583
    ): ?stdClass {
3584
        global $CFG;
3585
 
3586
        $acceptedsendoptions = [
3587
            ENROL_DO_NOT_SEND_EMAIL,
3588
            ENROL_SEND_EMAIL_FROM_COURSE_CONTACT,
3589
            ENROL_SEND_EMAIL_FROM_KEY_HOLDER,
3590
            ENROL_SEND_EMAIL_FROM_NOREPLY,
3591
        ];
3592
        if (!in_array($sendoption, $acceptedsendoptions)) {
3593
            throw new coding_exception('Invalid send option');
3594
        }
3595
        if ($sendoption === ENROL_DO_NOT_SEND_EMAIL) {
3596
            return null;
3597
        }
3598
        $contact = null;
3599
        // Send as the first user assigned as the course contact.
3600
        if ($sendoption === ENROL_SEND_EMAIL_FROM_COURSE_CONTACT) {
3601
            $rusers = [];
3602
            if (!empty($CFG->coursecontact)) {
3603
                $croles = explode(',', $CFG->coursecontact);
1441 ariadna 3604
                [$sort] = users_order_by_sql('u');
1 efrain 3605
                // We only use the first user.
3606
                $i = 0;
3607
                do {
3608
                    $userfieldsapi = \core_user\fields::for_name();
3609
                    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1441 ariadna 3610
                    $rusers = get_role_users($croles[$i], $context, true,
3611
                        "u.id, u.confirmed, u.username, {$allnames}, u.email, u.maildisplay, r.sortorder, ra.id AS raid",
3612
                        "r.sortorder, ra.id ASC, {$sort}");
1 efrain 3613
                    $i++;
3614
                } while (empty($rusers) && !empty($croles[$i]));
3615
            }
3616
            if ($rusers) {
3617
                $contact = array_values($rusers)[0];
3618
            }
3619
        } else if ($sendoption === ENROL_SEND_EMAIL_FROM_KEY_HOLDER) {
3620
            // Send as the first user with enrol/self:holdkey capability assigned in the course.
3621
            [$sort] = users_order_by_sql('u');
3622
            $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', 'u.*', $sort);
3623
            if (!empty($keyholders)) {
3624
                $contact = array_values($keyholders)[0];
3625
            }
3626
        }
3627
 
3628
        if ($sendoption === ENROL_SEND_EMAIL_FROM_NOREPLY) {
3629
            $contact = core_user::get_noreply_user();
3630
        }
3631
 
3632
        return $contact;
3633
    }
3634
 
3635
    /**
3636
     * Send course welcome message to user.
3637
     *
3638
     * @param stdClass $instance Enrol instance.
3639
     * @param int $userid User ID.
3640
     * @param int $sendoption Send email from constant ENROL_SEND_EMAIL_FROM_*
3641
     * @param null|string $message Message to send to the user.
1441 ariadna 3642
     * @param int|null $roleid The assigned role ID
1 efrain 3643
     */
3644
    public function send_course_welcome_message_to_user(
3645
        stdClass $instance,
3646
        int $userid,
3647
        int $sendoption,
3648
        ?string $message = '',
1441 ariadna 3649
        ?int $roleid = null,
1 efrain 3650
    ): void {
1441 ariadna 3651
        global $DB, $CFG;
3652
        require_once($CFG->dirroot . '/course/lib.php');
3653
 
1 efrain 3654
        $context = context_course::instance($instance->courseid);
3655
        $user = core_user::get_user($userid);
3656
        $course = get_course($instance->courseid);
3657
 
1441 ariadna 3658
        // Fallback to the instance role ID if parameter not specified.
3659
        $courseroleid = $roleid ?: $instance->roleid;
3660
        $courserole = $DB->get_record('role', ['id' => $courseroleid]);
3661
 
1 efrain 3662
        $a = new stdClass();
11 efrain 3663
        $a->coursename = format_string($course->fullname, true, ['context' => $context, 'escape' => false]);
1441 ariadna 3664
        $a->courselink = course_get_url($course)->out();
1 efrain 3665
        $a->profileurl = (new moodle_url(
3666
            url: '/user/view.php',
3667
            params: [
3668
                'id' => $user->id,
3669
                'course' => $instance->courseid,
3670
            ],
3671
        ))->out();
3672
 
1441 ariadna 3673
        $placeholders = \core_user::get_name_placeholders($user);
3674
        foreach ($placeholders as $field => $value) {
3675
            $a->{$field} = $value;
3676
        }
3677
 
1 efrain 3678
        if ($message && trim($message) !== '') {
3679
            $placeholders = [
3680
                '{$a->coursename}',
1441 ariadna 3681
                '{$a->courselink}',
1 efrain 3682
                '{$a->profileurl}',
3683
                '{$a->fullname}',
3684
                '{$a->email}',
3685
                '{$a->firstname}',
3686
                '{$a->lastname}',
3687
                '{$a->courserole}',
3688
            ];
3689
            $values = [
3690
                $a->coursename,
1441 ariadna 3691
                $a->courselink,
1 efrain 3692
                $a->profileurl,
3693
                fullname($user),
3694
                $user->email,
3695
                $user->firstname,
3696
                $user->lastname,
1441 ariadna 3697
                role_get_name($courserole, $context),
1 efrain 3698
            ];
3699
            $message = str_replace($placeholders, $values, $message);
3700
            if (strpos($message, '<') === false) {
3701
                // Plain text only.
3702
                $messagetext = $message;
3703
                $messagehtml = text_to_html($messagetext, null, false, true);
3704
            } else {
3705
                // This is most probably the tag/newline soup known as FORMAT_MOODLE.
3706
                $messagehtml = format_text($message, FORMAT_MOODLE,
3707
                    ['context' => $context, 'para' => false, 'newlines' => true, 'filter' => true]);
3708
                $messagetext = html_to_text($messagehtml);
3709
            }
3710
        } else {
3711
            $messagetext = get_string('customwelcomemessageplaceholder', 'core_enrol', $a);
3712
            $messagehtml = text_to_html($messagetext, null, false, true);
3713
        }
3714
 
3715
        $contact = $this->get_welcome_message_contact(
3716
            sendoption: $sendoption,
3717
            context: $context,
3718
        );
3719
        if (!$contact) {
3720
            // Cannot find the contact to send the message from.
3721
            return;
3722
        }
3723
 
3724
        $message = new \core\message\message();
3725
        $message->courseid = $instance->courseid;
3726
        $message->component = 'moodle';
3727
        $message->name = 'enrolcoursewelcomemessage';
3728
        $message->userfrom = $contact;
3729
        $message->userto = $user;
11 efrain 3730
        $message->subject = get_string('welcometocourse', 'moodle', $a->coursename);
1 efrain 3731
        $message->fullmessage = $messagetext;
3732
        $message->fullmessageformat = FORMAT_MARKDOWN;
3733
        $message->fullmessagehtml = $messagehtml;
3734
        $message->notification = 1;
3735
        $message->contexturl = $a->profileurl;
1441 ariadna 3736
        $message->contexturlname = $a->coursename;
1 efrain 3737
 
3738
        message_send($message);
3739
    }
3740
 
3741
    /**
3742
     * Updates enrol plugin instance with provided data.
3743
     * @param int $courseid Course ID.
3744
     * @param array $enrolmentdata enrolment data.
3745
     * @param stdClass $instance Instance to update.
3746
     *
3747
     * @return stdClass updated instance
3748
     */
3749
    public function update_enrol_plugin_data(int $courseid, array $enrolmentdata, stdClass $instance): stdClass {
3750
        global $DB;
3751
 
3752
        // Sort out the start, end and date.
3753
        $instance->enrolstartdate = (isset($enrolmentdata['startdate']) ? strtotime($enrolmentdata['startdate']) : 0);
3754
        $instance->enrolenddate = (isset($enrolmentdata['enddate']) ? strtotime($enrolmentdata['enddate']) : 0);
3755
 
3756
        // Is the enrolment period set?
3757
        if (!empty($enrolmentdata['enrolperiod'])) {
3758
            if (preg_match('/^\d+$/', $enrolmentdata['enrolperiod'])) {
3759
                $enrolmentdata['enrolperiod'] = (int)$enrolmentdata['enrolperiod'];
3760
            } else {
3761
                // Try and convert period to seconds.
3762
                $enrolmentdata['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $enrolmentdata['enrolperiod']);
3763
            }
3764
            $instance->enrolperiod = $enrolmentdata['enrolperiod'];
3765
        }
3766
        if ($instance->enrolstartdate > 0 && isset($enrolmentdata['enrolperiod'])) {
3767
            $instance->enrolenddate = $instance->enrolstartdate + $enrolmentdata['enrolperiod'];
3768
        }
3769
        if ($instance->enrolenddate > 0) {
3770
            $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
3771
        }
3772
        if ($instance->enrolenddate < $instance->enrolstartdate) {
3773
            $instance->enrolenddate = $instance->enrolstartdate;
3774
        }
3775
 
3776
        // Sort out the given role.
3777
        if (isset($enrolmentdata['role']) || isset($enrolmentdata['roleid'])) {
3778
            if (isset($enrolmentdata['role'])) {
3779
                $roleid = $DB->get_field('role', 'id', ['shortname' => $enrolmentdata['role']], MUST_EXIST);
3780
            } else {
3781
                $roleid = $enrolmentdata['roleid'];
3782
            }
3783
            $instance->roleid = $roleid;
3784
        }
3785
 
3786
        // Sort out custom instance name.
3787
        if (isset($enrolmentdata['name'])) {
3788
            $instance->name = $enrolmentdata['name'];
3789
        }
3790
        return $instance;
3791
    }
3792
}