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
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * External user API
19
 *
20
 * @package   core_user
21
 * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
1441 ariadna 25
use core\di;
26
use core\hook;
27
use core_user\hook\extend_user_menu;
28
 
1 efrain 29
define('USER_FILTER_ENROLMENT', 1);
30
define('USER_FILTER_GROUP', 2);
31
define('USER_FILTER_LAST_ACCESS', 3);
32
define('USER_FILTER_ROLE', 4);
33
define('USER_FILTER_STATUS', 5);
34
define('USER_FILTER_STRING', 6);
35
 
36
/**
37
 * Creates a user
38
 *
39
 * @throws moodle_exception
40
 * @param stdClass|array $user user to create
41
 * @param bool $updatepassword if true, authentication plugin will update password.
42
 * @param bool $triggerevent set false if user_created event should not be triggred.
43
 *             This will not affect user_password_updated event triggering.
44
 * @return int id of the newly created user
45
 */
46
function user_create_user($user, $updatepassword = true, $triggerevent = true) {
47
    global $DB;
48
 
49
    // Set the timecreate field to the current time.
50
    if (!is_object($user)) {
51
        $user = (object) $user;
52
    }
53
 
54
    // Check username.
55
    if (trim($user->username) === '') {
56
        throw new moodle_exception('invalidusernameblank');
57
    }
58
 
59
    if ($user->username !== core_text::strtolower($user->username)) {
60
        throw new moodle_exception('usernamelowercase');
61
    }
62
 
63
    if ($user->username !== core_user::clean_field($user->username, 'username')) {
64
        throw new moodle_exception('invalidusername');
65
    }
66
 
67
    // Save the password in a temp value for later.
68
    if ($updatepassword && isset($user->password)) {
69
 
70
        // Check password toward the password policy.
71
        if (!check_password_policy($user->password, $errmsg, $user)) {
72
            throw new moodle_exception($errmsg);
73
        }
74
 
75
        $userpassword = $user->password;
76
        unset($user->password);
77
    }
78
 
79
    // Apply default values for user preferences that are stored in users table.
80
    if (!isset($user->calendartype)) {
81
        $user->calendartype = core_user::get_property_default('calendartype');
82
    }
83
    if (!isset($user->maildisplay)) {
84
        $user->maildisplay = core_user::get_property_default('maildisplay');
85
    }
86
    if (!isset($user->mailformat)) {
87
        $user->mailformat = core_user::get_property_default('mailformat');
88
    }
89
    if (!isset($user->maildigest)) {
90
        $user->maildigest = core_user::get_property_default('maildigest');
91
    }
92
    if (!isset($user->autosubscribe)) {
93
        $user->autosubscribe = core_user::get_property_default('autosubscribe');
94
    }
95
    if (!isset($user->trackforums)) {
96
        $user->trackforums = core_user::get_property_default('trackforums');
97
    }
98
    if (!isset($user->lang)) {
99
        $user->lang = core_user::get_property_default('lang');
100
    }
101
    if (!isset($user->city)) {
102
        $user->city = core_user::get_property_default('city');
103
    }
104
    if (!isset($user->country)) {
105
        // The default value of $CFG->country is 0, but that isn't a valid property for the user field, so switch to ''.
106
        $user->country = core_user::get_property_default('country') ?: '';
107
    }
108
 
109
    $user->timecreated = time();
110
    $user->timemodified = $user->timecreated;
111
 
112
    // Validate user data object.
113
    $uservalidation = core_user::validate($user);
114
    if ($uservalidation !== true) {
115
        foreach ($uservalidation as $field => $message) {
116
            debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
117
            $user->$field = core_user::clean_field($user->$field, $field);
118
        }
119
    }
120
 
121
    // Insert the user into the database.
122
    $newuserid = $DB->insert_record('user', $user);
123
 
124
    // Create USER context for this user.
125
    $usercontext = context_user::instance($newuserid);
126
 
127
    // Update user password if necessary.
128
    if (isset($userpassword)) {
129
        // Get full database user row, in case auth is default.
130
        $newuser = $DB->get_record('user', array('id' => $newuserid));
131
        $authplugin = get_auth_plugin($newuser->auth);
132
        $authplugin->user_update_password($newuser, $userpassword);
133
    }
134
 
135
    // Trigger event If required.
136
    if ($triggerevent) {
137
        \core\event\user_created::create_from_userid($newuserid)->trigger();
138
    }
139
 
140
    // Purge the associated caches for the current user only.
141
    $presignupcache = \cache::make('core', 'presignup');
142
    $presignupcache->purge_current_user();
143
 
144
    return $newuserid;
145
}
146
 
147
/**
148
 * Update a user with a user object (will compare against the ID)
149
 *
150
 * @throws moodle_exception
151
 * @param stdClass|array $user the user to update
152
 * @param bool $updatepassword if true, authentication plugin will update password.
153
 * @param bool $triggerevent set false if user_updated event should not be triggred.
154
 *             This will not affect user_password_updated event triggering.
155
 */
156
function user_update_user($user, $updatepassword = true, $triggerevent = true) {
157
    global $DB;
158
 
159
    // Set the timecreate field to the current time.
160
    if (!is_object($user)) {
161
        $user = (object) $user;
162
    }
163
 
164
    $currentrecord = $DB->get_record('user', ['id' => $user->id]);
165
 
166
    // Dispatch the hook for pre user update actions.
167
    $hook = new \core_user\hook\before_user_updated(
168
        user: $user,
169
        currentuserdata: $currentrecord,
170
    );
171
    \core\di::get(\core\hook\manager::class)->dispatch($hook);
172
 
173
    // Check username.
174
    if (isset($user->username)) {
175
        if ($user->username !== core_text::strtolower($user->username)) {
176
            throw new moodle_exception('usernamelowercase');
177
        } else {
178
            if ($user->username !== core_user::clean_field($user->username, 'username')) {
179
                throw new moodle_exception('invalidusername');
180
            }
181
        }
182
    }
183
 
184
    // Unset password here, for updating later, if password update is required.
185
    if ($updatepassword && isset($user->password)) {
186
 
187
        // Check password toward the password policy.
188
        if (!check_password_policy($user->password, $errmsg, $user)) {
189
            throw new moodle_exception($errmsg);
190
        }
191
 
192
        $passwd = $user->password;
193
        unset($user->password);
194
    }
195
 
196
    // Make sure calendartype, if set, is valid.
197
    if (empty($user->calendartype)) {
198
        // Unset this variable, must be an empty string, which we do not want to update the calendartype to.
199
        unset($user->calendartype);
200
    }
201
 
202
    // Delete theme usage cache if the theme has been changed.
203
    if (isset($user->theme)) {
204
        if ($user->theme != $currentrecord->theme) {
205
            theme_delete_used_in_context_cache($user->theme, $currentrecord->theme);
206
        }
207
    }
208
 
209
    // Validate user data object.
210
    $uservalidation = core_user::validate($user);
211
    if ($uservalidation !== true) {
212
        foreach ($uservalidation as $field => $message) {
213
            debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
214
            $user->$field = core_user::clean_field($user->$field, $field);
215
        }
216
    }
217
 
218
    $changedattributes = [];
219
    foreach ($user as $attributekey => $attributevalue) {
220
        // We explicitly want to ignore 'timemodified' attribute for checking, if an update is needed.
221
        if (!property_exists($currentrecord, $attributekey) || $attributekey === 'timemodified') {
222
            continue;
223
        }
11 efrain 224
        if ($currentrecord->{$attributekey} !== $attributevalue) {
1 efrain 225
            $changedattributes[$attributekey] = $attributevalue;
226
        }
227
    }
228
    if (!empty($changedattributes)) {
229
        $changedattributes['timemodified'] = time();
230
        $updaterecord = (object) $changedattributes;
231
        $updaterecord->id = $currentrecord->id;
232
        $DB->update_record('user', $updaterecord);
233
    }
234
 
235
    if ($updatepassword) {
236
        // If there have been changes, update user record with changed attributes.
237
        if (!empty($changedattributes)) {
238
            foreach ($changedattributes as $attributekey => $attributevalue) {
239
                $currentrecord->{$attributekey} = $attributevalue;
240
            }
241
        }
242
 
243
        // If password was set, then update its hash.
244
        if (isset($passwd)) {
245
            $authplugin = get_auth_plugin($currentrecord->auth);
246
            if ($authplugin->can_change_password()) {
247
                $authplugin->user_update_password($currentrecord, $passwd);
248
            }
249
        }
250
    }
251
    // Trigger event if required.
252
    if ($triggerevent) {
253
        \core\event\user_updated::create_from_userid($user->id)->trigger();
254
    }
255
}
256
 
257
/**
258
 * Marks user deleted in internal user database and notifies the auth plugin.
259
 * Also unenrols user from all roles and does other cleanup.
260
 *
261
 * @todo Decide if this transaction is really needed (look for internal TODO:)
262
 * @param object $user Userobject before delete    (without system magic quotes)
263
 * @return boolean success
264
 */
265
function user_delete_user($user) {
266
    return delete_user($user);
267
}
268
 
269
/**
270
 * Get users by id
271
 *
272
 * @param array $userids id of users to retrieve
273
 * @return array
274
 */
275
function user_get_users_by_id($userids) {
276
    global $DB;
277
    return $DB->get_records_list('user', 'id', $userids);
278
}
279
 
280
/**
281
 * Returns the list of default 'displayable' fields
282
 *
283
 * Contains database field names but also names used to generate information, such as enrolledcourses
284
 *
285
 * @return array of user fields
286
 */
287
function user_get_default_fields() {
288
    return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email',
289
        'address', 'phone1', 'phone2', 'department',
290
        'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
291
        'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
292
        'city', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
293
        'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended', 'lastcourseaccess', 'trackforums',
294
    );
295
}
296
 
297
/**
298
 *
299
 * Give user record from mdl_user, build an array contains all user details.
300
 *
301
 * Warning: description file urls are 'webservice/pluginfile.php' is use.
302
 *          it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
303
 *
304
 * @throws moodle_exception
305
 * @param stdClass $user user record from mdl_user
306
 * @param stdClass $course moodle course
307
 * @param array $userfields required fields
308
 * @return array|null
309
 */
310
function user_get_user_details($user, $course = null, array $userfields = array()) {
311
    global $USER, $DB, $CFG, $PAGE;
312
    require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library.
313
    require_once($CFG->dirroot . "/lib/filelib.php");      // File handling on description and friends.
314
 
315
    $defaultfields = user_get_default_fields();
316
 
317
    if (empty($userfields)) {
318
        $userfields = $defaultfields;
319
    }
320
 
321
    foreach ($userfields as $thefield) {
322
        if (!in_array($thefield, $defaultfields)) {
323
            throw new moodle_exception('invaliduserfield', 'error', '', $thefield);
324
        }
325
    }
326
 
327
    // Make sure id and fullname are included.
328
    if (!in_array('id', $userfields)) {
329
        $userfields[] = 'id';
330
    }
331
 
332
    if (!in_array('fullname', $userfields)) {
333
        $userfields[] = 'fullname';
334
    }
335
 
1441 ariadna 336
    // Callback check for plugins to allow or prevent access.
337
    $forceallow = true;
338
    $currentuser = ($user->id == $USER->id);
339
    $isadmin = is_siteadmin($USER);
340
    if (!$currentuser) {
341
        $forceallow = false;
342
        $callbackresult = user_process_profile_callbacks($user, $course);
343
        if ($callbackresult === core_user::VIEWPROFILE_PREVENT) {
344
            return null; // Access denied.
345
        } else if ($callbackresult === core_user::VIEWPROFILE_FORCE_ALLOW) {
346
            $forceallow = true;
347
        }
348
    }
349
 
1 efrain 350
    if (!empty($course)) {
351
        $context = context_course::instance($course->id);
352
        $usercontext = context_user::instance($user->id);
353
    } else {
354
        $context = context_user::instance($user->id);
355
        $usercontext = $context;
356
    }
357
 
1441 ariadna 358
    if (!$forceallow) {
359
        // Existing capability checks.
360
        if (!empty($course)) {
361
            $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
362
        } else {
363
            $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
364
        }
1 efrain 365
 
1441 ariadna 366
        if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) {
367
            // Skip this user details.
368
            return null;
369
        }
370
    }
371
 
1 efrain 372
    // This does not need to include custom profile fields as it is only used to check specific
373
    // fields below.
374
    $showuseridentityfields = \core_user\fields::get_identity_fields($context, false);
375
 
376
    if (!empty($course)) {
377
        $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
378
    } else {
379
        $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
380
    }
381
    $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
382
    if (!empty($course)) {
383
        $canviewuseremail = has_capability('moodle/course:useremail', $context);
384
    } else {
385
        $canviewuseremail = false;
386
    }
387
    $cannotviewdescription   = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
388
    if (!empty($course)) {
389
        $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
390
    } else {
391
        $canaccessallgroups = false;
392
    }
393
 
1441 ariadna 394
    // User ID and fullname are always included.
395
    $userdetails = [
396
        'id' => $user->id,
397
        'fullname' => fullname($user, $canviewfullnames),
398
    ];
399
 
400
    // User first/lastname included if capability check passes, or the same is present in fullname.
401
    $dummyusername = core_user::get_dummy_fullname($context, ['override' => $canviewfullnames]);
402
    if (in_array('firstname', $userfields) &&
403
            ($canviewfullnames || core_text::strrpos($dummyusername, 'firstname') !== false)) {
404
        $userdetails['firstname'] = $user->firstname;
1 efrain 405
    }
1441 ariadna 406
    if (in_array('lastname', $userfields) &&
407
            ($canviewfullnames || core_text::strrpos($dummyusername, 'lastname') !== false)) {
408
        $userdetails['lastname'] = $user->lastname;
409
    }
1 efrain 410
 
411
    if (in_array('username', $userfields)) {
412
        if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
413
            $userdetails['username'] = $user->username;
414
        }
415
    }
416
 
417
    if (in_array('customfields', $userfields)) {
418
        $categories = profile_get_user_fields_with_data_by_category($user->id);
419
        $userdetails['customfields'] = array();
420
        foreach ($categories as $categoryid => $fields) {
421
            foreach ($fields as $formfield) {
422
                if ($formfield->show_field_content()) {
423
                    $userdetails['customfields'][] = [
424
                        'name' => $formfield->display_name(),
425
                        'value' => $formfield->data,
426
                        'displayvalue' => $formfield->display_data(),
427
                        'type' => $formfield->field->datatype,
428
                        'shortname' => $formfield->field->shortname
429
                    ];
430
                }
431
            }
432
        }
433
        // Unset customfields if it's empty.
434
        if (empty($userdetails['customfields'])) {
435
            unset($userdetails['customfields']);
436
        }
437
    }
438
 
439
    // Profile image.
440
    if (in_array('profileimageurl', $userfields)) {
441
        $userpicture = new user_picture($user);
442
        $userpicture->size = 1; // Size f1.
443
        $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
444
    }
445
    if (in_array('profileimageurlsmall', $userfields)) {
446
        if (!isset($userpicture)) {
447
            $userpicture = new user_picture($user);
448
        }
449
        $userpicture->size = 0; // Size f2.
450
        $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
451
    }
452
 
453
    // Hidden user field.
454
    if ($canviewhiddenuserfields) {
455
        $hiddenfields = array();
456
    } else {
457
        $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
458
    }
459
 
11 efrain 460
    if (!empty($user->address) && (in_array('address', $userfields) || $isadmin)) {
1 efrain 461
        $userdetails['address'] = $user->address;
462
    }
463
    if (!empty($user->phone1) && (in_array('phone1', $userfields)
464
            && in_array('phone1', $showuseridentityfields) || $isadmin)) {
465
        $userdetails['phone1'] = $user->phone1;
466
    }
467
    if (!empty($user->phone2) && (in_array('phone2', $userfields)
468
            && in_array('phone2', $showuseridentityfields) || $isadmin)) {
469
        $userdetails['phone2'] = $user->phone2;
470
    }
471
 
472
    if (isset($user->description) &&
473
        ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
474
        if (in_array('description', $userfields)) {
475
            // Always return the descriptionformat if description is requested.
476
            list($userdetails['description'], $userdetails['descriptionformat']) =
477
                    \core_external\util::format_text($user->description, $user->descriptionformat,
478
                            $usercontext, 'user', 'profile', null);
479
        }
480
    }
481
 
482
    if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
483
        $userdetails['country'] = $user->country;
484
    }
485
 
486
    if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
487
        $userdetails['city'] = $user->city;
488
    }
489
 
490
    if (in_array('timezone', $userfields) && (!isset($hiddenfields['timezone']) || $isadmin) && $user->timezone) {
491
        $userdetails['timezone'] = $user->timezone;
492
    }
493
 
494
    if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
495
        $userdetails['suspended'] = (bool)$user->suspended;
496
    }
497
 
498
    if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
499
        if ($user->firstaccess) {
500
            $userdetails['firstaccess'] = $user->firstaccess;
501
        } else {
502
            $userdetails['firstaccess'] = 0;
503
        }
504
    }
505
    if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
506
        if ($user->lastaccess) {
507
            $userdetails['lastaccess'] = $user->lastaccess;
508
        } else {
509
            $userdetails['lastaccess'] = 0;
510
        }
511
    }
512
 
513
    // Hidden fields restriction to lastaccess field applies to both site and course access time.
514
    if (in_array('lastcourseaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
515
        if (isset($user->lastcourseaccess)) {
516
            $userdetails['lastcourseaccess'] = $user->lastcourseaccess;
517
        } else {
518
            $userdetails['lastcourseaccess'] = 0;
519
        }
520
    }
521
 
522
    if (in_array('email', $userfields) && (
523
            $currentuser
524
            or (!isset($hiddenfields['email']) and (
525
                $user->maildisplay == core_user::MAILDISPLAY_EVERYONE
526
                or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER))
527
                or $canviewuseremail  // TODO: Deprecate/remove for MDL-37479.
528
            ))
529
            or in_array('email', $showuseridentityfields)
530
       )) {
531
        $userdetails['email'] = $user->email;
532
    }
533
 
534
    if (in_array('interests', $userfields)) {
535
        $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
536
        if ($interests) {
537
            $userdetails['interests'] = join(', ', $interests);
538
        }
539
    }
540
 
541
    // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
542
    if (in_array('idnumber', $userfields) && $user->idnumber) {
543
        if (in_array('idnumber', $showuseridentityfields) or $currentuser or
544
                has_capability('moodle/user:viewalldetails', $context)) {
545
            $userdetails['idnumber'] = $user->idnumber;
546
        }
547
    }
548
    if (in_array('institution', $userfields) && $user->institution) {
549
        if (in_array('institution', $showuseridentityfields) or $currentuser or
550
                has_capability('moodle/user:viewalldetails', $context)) {
551
            $userdetails['institution'] = $user->institution;
552
        }
553
    }
554
    // Isset because it's ok to have department 0.
555
    if (in_array('department', $userfields) && isset($user->department)) {
556
        if (in_array('department', $showuseridentityfields) or $currentuser or
557
                has_capability('moodle/user:viewalldetails', $context)) {
558
            $userdetails['department'] = $user->department;
559
        }
560
    }
561
 
562
    if (in_array('roles', $userfields) && !empty($course)) {
563
        // Not a big secret.
564
        $roles = get_user_roles($context, $user->id, false);
565
        $userdetails['roles'] = array();
566
        foreach ($roles as $role) {
567
            $userdetails['roles'][] = array(
568
                'roleid'       => $role->roleid,
569
                'name'         => $role->name,
570
                'shortname'    => $role->shortname,
571
                'sortorder'    => $role->sortorder
572
            );
573
        }
574
    }
575
 
576
    // Return user groups.
577
    if (in_array('groups', $userfields) && !empty($course)) {
578
        if ($usergroups = groups_get_all_groups($course->id, $user->id)) {
579
            $userdetails['groups'] = [];
580
            foreach ($usergroups as $group) {
581
                if ($course->groupmode == SEPARATEGROUPS && !$canaccessallgroups && $user->id != $USER->id) {
582
                    // In separate groups, I only have to see the groups shared between both users.
583
                    if (!groups_is_member($group->id, $USER->id)) {
584
                        continue;
585
                    }
586
                }
587
 
1441 ariadna 588
                $groupdescription = file_rewrite_pluginfile_urls($group->description, 'pluginfile.php', $context->id, 'group',
589
                    'description', $group->id);
590
 
1 efrain 591
                $userdetails['groups'][] = [
592
                    'id' => $group->id,
1441 ariadna 593
                    'name' => format_string($group->name, true, ['context' => $context]),
594
                    'description' => format_text($groupdescription, $group->descriptionformat, ['context' => $context]),
1 efrain 595
                    'descriptionformat' => $group->descriptionformat
596
                ];
597
            }
598
        }
599
    }
600
    // List of courses where the user is enrolled.
601
    if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
602
        $enrolledcourses = array();
603
        if ($mycourses = enrol_get_users_courses($user->id, true)) {
604
            foreach ($mycourses as $mycourse) {
605
                if ($mycourse->category) {
606
                    $coursecontext = context_course::instance($mycourse->id);
607
                    $enrolledcourse = array();
608
                    $enrolledcourse['id'] = $mycourse->id;
609
                    $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
610
                    $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
611
                    $enrolledcourses[] = $enrolledcourse;
612
                }
613
            }
614
            $userdetails['enrolledcourses'] = $enrolledcourses;
615
        }
616
    }
617
 
618
    // User preferences.
619
    if (in_array('preferences', $userfields) && $currentuser) {
620
        $preferences = array();
621
        $userpreferences = get_user_preferences();
622
        foreach ($userpreferences as $prefname => $prefvalue) {
623
            $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
624
        }
625
        $userdetails['preferences'] = $preferences;
626
    }
627
 
628
    if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
629
        $extrafields = ['auth', 'confirmed', 'lang', 'theme', 'mailformat', 'trackforums'];
630
        foreach ($extrafields as $extrafield) {
631
            if (in_array($extrafield, $userfields) && isset($user->$extrafield)) {
632
                $userdetails[$extrafield] = $user->$extrafield;
633
            }
634
        }
635
    }
636
 
637
    // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
638
    if (isset($userdetails['lang'])) {
639
        $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG);
640
    }
641
    if (isset($userdetails['theme'])) {
642
        $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME);
643
    }
644
 
645
    return $userdetails;
646
}
647
 
648
/**
649
 * Tries to obtain user details, either recurring directly to the user's system profile
650
 * or through one of the user's course enrollments (course profile).
651
 *
652
 * You can use the $userfields parameter to reduce the amount of a user record that is required by the method.
653
 * The minimum user fields are:
654
 *  * id
655
 *  * deleted
656
 *  * all potential fullname fields
657
 *
658
 * @param stdClass $user The user.
659
 * @param array $userfields An array of userfields to be returned, the values must be a
660
 *                          subset of user_get_default_fields (optional)
661
 * @return array if unsuccessful or the allowed user details.
662
 */
663
function user_get_user_details_courses($user, array $userfields = []) {
664
    global $USER;
665
    $userdetails = null;
666
 
667
    $systemprofile = false;
668
    if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
669
        $systemprofile = true;
670
    }
671
 
672
    // Try using system profile.
673
    if ($systemprofile) {
674
        $userdetails = user_get_user_details($user, null, $userfields);
675
    } else {
676
        // Try through course profile.
677
        // Get the courses that the user is enrolled in (only active).
678
        $courses = enrol_get_users_courses($user->id, true);
679
        foreach ($courses as $course) {
680
            if (user_can_view_profile($user, $course)) {
681
                $userdetails = user_get_user_details($user, $course, $userfields);
682
            }
683
        }
684
    }
685
 
686
    return $userdetails;
687
}
688
 
689
/**
690
 * Check if $USER have the necessary capabilities to obtain user details.
691
 *
692
 * @param stdClass $user
693
 * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
694
 * @return bool true if $USER can view user details.
695
 */
696
function can_view_user_details_cap($user, $course = null) {
697
    // Check $USER has the capability to view the user details at user context.
698
    $usercontext = context_user::instance($user->id);
699
    $result = has_capability('moodle/user:viewdetails', $usercontext);
700
    // Otherwise can $USER see them at course context.
701
    if (!$result && !empty($course)) {
702
        $context = context_course::instance($course->id);
703
        $result = has_capability('moodle/user:viewdetails', $context);
704
    }
705
    return $result;
706
}
707
 
708
/**
709
 * Return a list of page types
710
 * @param string $pagetype current page type
711
 * @param stdClass $parentcontext Block's parent context
712
 * @param stdClass $currentcontext Current context of block
713
 * @return array
714
 */
715
function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
716
    return array('user-profile' => get_string('page-user-profile', 'pagetype'));
717
}
718
 
719
/**
720
 * Count the number of failed login attempts for the given user, since last successful login.
721
 *
722
 * @param int|stdclass $user user id or object.
723
 * @param bool $reset Resets failed login count, if set to true.
724
 *
725
 * @return int number of failed login attempts since the last successful login.
726
 */
727
function user_count_login_failures($user, $reset = true) {
728
    global $DB;
729
 
730
    if (!is_object($user)) {
731
        $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
732
    }
733
    if ($user->deleted) {
734
        // Deleted user, nothing to do.
735
        return 0;
736
    }
737
    $count = get_user_preferences('login_failed_count_since_success', 0, $user);
738
    if ($reset) {
739
        set_user_preference('login_failed_count_since_success', 0, $user);
740
    }
741
    return $count;
742
}
743
 
744
/**
745
 * Converts a string into a flat array of menu items, where each menu items is a
746
 * stdClass with fields type, url, title.
747
 *
748
 * @param string $text the menu items definition
749
 * @param moodle_page $page the current page
750
 * @return array
751
 */
752
function user_convert_text_to_menu_items($text, $page) {
753
    $lines = explode("\n", $text);
754
    $children = array();
755
    foreach ($lines as $line) {
756
        $line = trim($line);
757
        $bits = explode('|', $line, 2);
758
        $itemtype = 'link';
759
        if (preg_match("/^#+$/", $line)) {
760
            $itemtype = 'divider';
761
        } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
762
            // Every item must have a name to be valid.
763
            continue;
764
        } else {
765
            $bits[0] = ltrim($bits[0], '-');
766
        }
767
 
768
        // Create the child.
769
        $child = new stdClass();
770
        $child->itemtype = $itemtype;
771
        if ($itemtype === 'divider') {
772
            // Add the divider to the list of children and skip link
773
            // processing.
774
            $children[] = $child;
775
            continue;
776
        }
777
 
778
        // Name processing.
779
        $namebits = explode(',', $bits[0], 2);
780
        if (count($namebits) == 2) {
781
            $namebits[1] = $namebits[1] ?: 'core';
782
            // Check the validity of the identifier part of the string.
783
            if (clean_param($namebits[0], PARAM_STRINGID) !== '' && clean_param($namebits[1], PARAM_COMPONENT) !== '') {
784
                // Treat this as a language string.
785
                $child->title = get_string($namebits[0], $namebits[1]);
786
                $child->titleidentifier = implode(',', $namebits);
787
            }
788
        }
789
        if (empty($child->title)) {
790
            // Use it as is, don't even clean it.
791
            $child->title = $bits[0];
792
            $child->titleidentifier = str_replace(" ", "-", $bits[0]);
793
        }
794
 
795
        // URL processing.
796
        if (!array_key_exists(1, $bits) or empty($bits[1])) {
797
            // Set the url to null, and set the itemtype to invalid.
798
            $bits[1] = null;
799
            $child->itemtype = "invalid";
800
        } else {
801
            // Nasty hack to replace the grades with the direct url.
802
            if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
803
                $bits[1] = user_mygrades_url();
804
            }
805
 
806
            // Make sure the url is a moodle url.
807
            $bits[1] = new moodle_url(trim($bits[1]));
808
        }
809
        $child->url = $bits[1];
810
 
811
        // Add this child to the list of children.
812
        $children[] = $child;
813
    }
814
    return $children;
815
}
816
 
817
/**
818
 * Get a list of essential user navigation items.
819
 *
820
 * @param stdclass $user user object.
821
 * @param moodle_page $page page object.
822
 * @param array $options associative array.
823
 *     options are:
824
 *     - avatarsize=35 (size of avatar image)
825
 * @return stdClass $returnobj navigation information object, where:
826
 *
827
 *      $returnobj->navitems    array    array of links where each link is a
828
 *                                       stdClass with fields url, title, and
829
 *                                       pix
830
 *      $returnobj->metadata    array    array of useful user metadata to be
831
 *                                       used when constructing navigation;
832
 *                                       fields include:
833
 *
834
 *          ROLE FIELDS
835
 *          asotherrole    bool    whether viewing as another role
836
 *          rolename       string  name of the role
837
 *
838
 *          USER FIELDS
839
 *          These fields are for the currently-logged in user, or for
840
 *          the user that the real user is currently logged in as.
841
 *
842
 *          userid         int        the id of the user in question
843
 *          userfullname   string     the user's full name
844
 *          userprofileurl moodle_url the url of the user's profile
845
 *          useravatar     string     a HTML fragment - the rendered
846
 *                                    user_picture for this user
847
 *          userloginfail  string     an error string denoting the number
848
 *                                    of login failures since last login
849
 *
850
 *          "REAL USER" FIELDS
851
 *          These fields are for when asotheruser is true, and
852
 *          correspond to the underlying "real user".
853
 *
854
 *          asotheruser        bool    whether viewing as another user
855
 *          realuserid         int        the id of the user in question
856
 *          realuserfullname   string     the user's full name
857
 *          realuserprofileurl moodle_url the url of the user's profile
858
 *          realuseravatar     string     a HTML fragment - the rendered
859
 *                                        user_picture for this user
860
 *
861
 *          MNET PROVIDER FIELDS
862
 *          asmnetuser            bool   whether viewing as a user from an
863
 *                                       MNet provider
864
 *          mnetidprovidername    string name of the MNet provider
865
 *          mnetidproviderwwwroot string URL of the MNet provider
866
 */
867
function user_get_user_navigation_info($user, $page, $options = array()) {
868
    global $OUTPUT, $DB, $SESSION, $CFG;
869
 
870
    $returnobject = new stdClass();
871
    $returnobject->navitems = array();
872
    $returnobject->metadata = array();
873
 
874
    $guest = isguestuser();
875
    if (!isloggedin() || $guest) {
876
        $returnobject->unauthenticateduser = [
877
            'guest' => $guest,
878
            'content' => $guest ? 'loggedinasguest' : 'loggedinnot',
879
        ];
880
 
881
        return $returnobject;
882
    }
883
 
884
    $course = $page->course;
885
 
886
    // Query the environment.
887
    $context = context_course::instance($course->id);
888
 
889
    // Get basic user metadata.
890
    $returnobject->metadata['userid'] = $user->id;
891
    $returnobject->metadata['userfullname'] = fullname($user);
892
    $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
893
        'id' => $user->id
894
    ));
895
 
896
    $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
897
    if (!empty($options['avatarsize'])) {
898
        $avataroptions['size'] = $options['avatarsize'];
899
    }
900
    $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
901
        $user, $avataroptions
902
    );
903
    // Build a list of items for a regular user.
904
 
905
    // Query MNet status.
906
    if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
907
        $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
908
        $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
909
        $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
910
    }
911
 
912
    // Did the user just log in?
913
    if (isset($SESSION->justloggedin)) {
914
        // Don't unset this flag as login_info still needs it.
915
        if (!empty($CFG->displayloginfailures)) {
916
            // Don't reset the count either, as login_info() still needs it too.
917
            if ($count = user_count_login_failures($user, false)) {
918
 
919
                // Get login failures string.
920
                $a = new stdClass();
1441 ariadna 921
                $a->attempts = html_writer::tag('span', $count, array('class' => 'value me-1 fw-bold'));
1 efrain 922
                $returnobject->metadata['userloginfail'] =
923
                    get_string('failedloginattempts', '', $a);
924
 
925
            }
926
        }
927
    }
928
 
929
    $returnobject->metadata['asotherrole'] = false;
930
 
931
    // Before we add the last items (usually a logout + switch role link), add any
932
    // custom-defined items.
933
    $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
934
    $custommenucount = 0;
935
    foreach ($customitems as $item) {
936
        $returnobject->navitems[] = $item;
937
        if ($item->itemtype !== 'divider' && $item->itemtype !== 'invalid') {
938
            $custommenucount++;
939
        }
940
    }
941
 
1441 ariadna 942
    // Call to hook to add menu items.
943
    $hook = new extend_user_menu();
944
    di::get(core\hook\manager::class)->dispatch($hook);
945
    $hookitems = $hook->get_navitems();
946
    foreach ($hookitems as $menuitem) {
947
        $returnobject->navitems[] = $menuitem;
948
    }
949
 
1 efrain 950
    if ($custommenucount > 0) {
951
        // Only add a divider if we have customusermenuitems.
952
        $divider = new stdClass();
953
        $divider->itemtype = 'divider';
954
        $returnobject->navitems[] = $divider;
955
    }
956
 
957
    // Links: Preferences.
958
    $preferences = new stdClass();
959
    $preferences->itemtype = 'link';
960
    $preferences->url = new moodle_url('/user/preferences.php');
961
    $preferences->title = get_string('preferences');
962
    $preferences->titleidentifier = 'preferences,moodle';
963
    $returnobject->navitems[] = $preferences;
964
 
965
 
966
    if (is_role_switched($course->id)) {
967
        if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
968
            // Build role-return link instead of logout link.
969
            $rolereturn = new stdClass();
970
            $rolereturn->itemtype = 'link';
971
            $rolereturn->url = new moodle_url('/course/switchrole.php', array(
972
                'id' => $course->id,
973
                'sesskey' => sesskey(),
974
                'switchrole' => 0,
975
                'returnurl' => $page->url->out_as_local_url(false)
976
            ));
977
            $rolereturn->title = get_string('switchrolereturn');
978
            $rolereturn->titleidentifier = 'switchrolereturn,moodle';
979
            $returnobject->navitems[] = $rolereturn;
980
 
981
            $returnobject->metadata['asotherrole'] = true;
982
            $returnobject->metadata['rolename'] = role_get_name($role, $context);
983
 
984
        }
985
    } else {
986
        // Build switch role link.
987
        $roles = get_switchable_roles($context);
988
        if (is_array($roles) && (count($roles) > 0)) {
989
            $switchrole = new stdClass();
990
            $switchrole->itemtype = 'link';
991
            $switchrole->url = new moodle_url('/course/switchrole.php', array(
992
                'id' => $course->id,
993
                'switchrole' => -1,
994
                'returnurl' => $page->url->out_as_local_url(false)
995
            ));
996
            $switchrole->title = get_string('switchroleto');
997
            $switchrole->titleidentifier = 'switchroleto,moodle';
998
            $returnobject->navitems[] = $switchrole;
999
        }
1000
    }
1001
 
1002
    if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
1003
        $realuser = \core\session\manager::get_realuser();
1004
 
1005
        // Save values for the real user, as $user will be full of data for the
1006
        // user is disguised as.
1007
        $returnobject->metadata['realuserid'] = $realuser->id;
1008
        $returnobject->metadata['realuserfullname'] = fullname($realuser);
1009
        $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', [
1010
            'id' => $realuser->id
1011
        ]);
1012
        $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
1013
 
1014
        // Build a user-revert link.
1015
        $userrevert = new stdClass();
1016
        $userrevert->itemtype = 'link';
1017
        $userrevert->url = new moodle_url('/course/loginas.php', [
1018
            'id' => $course->id,
1019
            'sesskey' => sesskey()
1020
        ]);
1021
        $userrevert->title = get_string('logout');
1022
        $userrevert->titleidentifier = 'logout,moodle';
1023
        $returnobject->navitems[] = $userrevert;
1024
    } else {
1025
        // Build a logout link.
1026
        $logout = new stdClass();
1027
        $logout->itemtype = 'link';
1028
        $logout->url = new moodle_url('/login/logout.php', ['sesskey' => sesskey()]);
1029
        $logout->title = get_string('logout');
1030
        $logout->titleidentifier = 'logout,moodle';
1031
        $returnobject->navitems[] = $logout;
1032
    }
1033
 
1034
    return $returnobject;
1035
}
1036
 
1037
/**
1038
 * Add password to the list of used hashes for this user.
1039
 *
1040
 * This is supposed to be used from:
1041
 *  1/ change own password form
1042
 *  2/ password reset process
1043
 *  3/ user signup in auth plugins if password changing supported
1044
 *
1045
 * @param int $userid user id
1046
 * @param string $password plaintext password
1047
 * @return void
1048
 */
1049
function user_add_password_history(int $userid, #[\SensitiveParameter] string $password): void {
1050
    global $CFG, $DB;
1051
 
1052
    if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1053
        return;
1054
    }
1055
 
1056
    // Note: this is using separate code form normal password hashing because
1057
    // we need to have this under control in the future. Also, the auth
1058
    // plugin might not store the passwords locally at all.
1059
 
1060
    // First generate a cryptographically suitable salt.
1061
    $randombytes = random_bytes(16);
1062
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
1063
    // Then create the hash.
1064
    $generatedhash = crypt($password, '$6$rounds=10000$' . $salt . '$');
1065
 
1066
    $record = new stdClass();
1067
    $record->userid = $userid;
1068
    $record->hash = $generatedhash;
1069
    $record->timecreated = time();
1070
    $DB->insert_record('user_password_history', $record);
1071
 
1072
    $i = 0;
1073
    $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1074
    foreach ($records as $record) {
1075
        $i++;
1076
        if ($i > $CFG->passwordreuselimit) {
1077
            $DB->delete_records('user_password_history', array('id' => $record->id));
1078
        }
1079
    }
1080
}
1081
 
1082
/**
1083
 * Was this password used before on change or reset password page?
1084
 *
1085
 * The $CFG->passwordreuselimit setting determines
1086
 * how many times different password needs to be used
1087
 * before allowing previously used password again.
1088
 *
1089
 * @param int $userid user id
1090
 * @param string $password plaintext password
1091
 * @return bool true if password reused
1092
 */
1093
function user_is_previously_used_password($userid, $password) {
1094
    global $CFG, $DB;
1095
 
1096
    if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1097
        return false;
1098
    }
1099
 
1100
    $reused = false;
1101
 
1102
    $i = 0;
1103
    $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1104
    foreach ($records as $record) {
1105
        $i++;
1106
        if ($i > $CFG->passwordreuselimit) {
1107
            $DB->delete_records('user_password_history', array('id' => $record->id));
1108
            continue;
1109
        }
1110
        // NOTE: this is slow but we cannot compare the hashes directly any more.
1111
        if (password_verify($password, $record->hash)) {
1112
            $reused = true;
1113
        }
1114
    }
1115
 
1116
    return $reused;
1117
}
1118
 
1119
/**
1120
 * Remove a user device from the Moodle database (for PUSH notifications usually).
1121
 *
1122
 * @param string $uuid The device UUID.
1123
 * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1124
 * @return bool true if removed, false if the device didn't exists in the database
1125
 * @since Moodle 2.9
1126
 */
1127
function user_remove_user_device($uuid, $appid = "") {
1128
    global $DB, $USER;
1129
 
1130
    $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1131
    if (!empty($appid)) {
1132
        $conditions['appid'] = $appid;
1133
    }
1134
 
1135
    if (!$DB->count_records('user_devices', $conditions)) {
1136
        return false;
1137
    }
1138
 
1139
    $DB->delete_records('user_devices', $conditions);
1140
 
1141
    return true;
1142
}
1143
 
1144
/**
1145
 * Trigger user_list_viewed event.
1146
 *
1147
 * @param stdClass  $course course  object
1148
 * @param stdClass  $context course context object
1149
 * @since Moodle 2.9
1150
 */
1151
function user_list_view($course, $context) {
1152
 
1153
    $event = \core\event\user_list_viewed::create(array(
1154
        'objectid' => $course->id,
1155
        'courseid' => $course->id,
1156
        'context' => $context,
1157
        'other' => array(
1158
            'courseshortname' => $course->shortname,
1159
            'coursefullname' => $course->fullname
1160
        )
1161
    ));
1162
    $event->trigger();
1163
}
1164
 
1165
/**
1166
 * Returns the url to use for the "Grades" link in the user navigation.
1167
 *
1168
 * @param int $userid The user's ID.
1169
 * @param int $courseid The course ID if available.
1170
 * @return mixed A URL to be directed to for "Grades".
1171
 */
1172
function user_mygrades_url($userid = null, $courseid = SITEID) {
1173
    global $CFG, $USER;
1174
    $url = null;
1175
    if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1176
        if (isset($userid) && $USER->id != $userid) {
1177
            // Send to the gradebook report.
1178
            $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1179
                    array('id' => $courseid, 'userid' => $userid));
1180
        } else {
1181
            $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1182
        }
1183
    } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1184
            && !empty($CFG->gradereport_mygradeurl)) {
1185
        $url = $CFG->gradereport_mygradeurl;
1186
    } else {
1187
        $url = $CFG->wwwroot;
1188
    }
1189
    return $url;
1190
}
1191
 
1192
/**
1193
 * Check if the current user has permission to view details of the supplied user.
1194
 *
1195
 * This function supports two modes:
1196
 * If the optional $course param is omitted, then this function finds all shared courses and checks whether the current user has
1197
 * permission in any of them, returning true if so.
1198
 * If the $course param is provided, then this function checks permissions in ONLY that course.
1199
 *
1200
 * @param object $user The other user's details.
1201
 * @param object $course if provided, only check permissions in this course.
1202
 * @param context $usercontext The user context if available.
1203
 * @return bool true for ability to view this user, else false.
1204
 */
1205
function user_can_view_profile($user, $course = null, $usercontext = null) {
1206
    global $USER, $CFG;
1207
 
1208
    if ($user->deleted) {
1209
        return false;
1210
    }
1211
 
1212
    // Do we need to be logged in?
1213
    if (empty($CFG->forceloginforprofiles)) {
1214
        return true;
1215
    } else {
1216
       if (!isloggedin() || isguestuser()) {
1217
            // User is not logged in and forceloginforprofile is set, we need to return now.
1218
            return false;
1219
        }
1220
    }
1221
 
1222
    // Current user can always view their profile.
1223
    if ($USER->id == $user->id) {
1224
        return true;
1225
    }
1226
 
1227
    // Use callbacks so that (primarily) local plugins can prevent or allow profile access.
1441 ariadna 1228
    $callbackresult = user_process_profile_callbacks($user, $course, $usercontext);
1229
    if ($callbackresult === core_user::VIEWPROFILE_PREVENT) {
1230
        return false; // Access denied.
1231
    } else if ($callbackresult === core_user::VIEWPROFILE_FORCE_ALLOW) {
1 efrain 1232
        return true;
1233
    }
1234
 
1235
    // Course contacts have visible profiles always.
1236
    if (has_coursecontact_role($user->id)) {
1237
        return true;
1238
    }
1239
 
1240
    // If we're only checking the capabilities in the single provided course.
1241
    if (isset($course)) {
1242
        // Confirm that $user is enrolled in the $course we're checking.
1243
        if (is_enrolled(context_course::instance($course->id), $user)) {
1244
            $userscourses = array($course);
1245
        }
1246
    } else {
1247
        // Else we're checking whether the current user can view $user's profile anywhere, so check user context first.
1248
        if (empty($usercontext)) {
1249
            $usercontext = context_user::instance($user->id);
1250
        }
1251
        if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) {
1252
            return true;
1253
        }
1254
        // This returns context information, so we can preload below.
1255
        $userscourses = enrol_get_all_users_courses($user->id);
1256
    }
1257
 
1258
    if (empty($userscourses)) {
1259
        return false;
1260
    }
1261
 
1262
    foreach ($userscourses as $userscourse) {
1263
        context_helper::preload_from_record($userscourse);
1264
        $coursecontext = context_course::instance($userscourse->id);
1265
        if (has_capability('moodle/user:viewdetails', $coursecontext) ||
1266
            has_capability('moodle/user:viewalldetails', $coursecontext)) {
1267
            if (!groups_user_groups_visible($userscourse, $user->id)) {
1268
                // Not a member of the same group.
1269
                continue;
1270
            }
1271
            return true;
1272
        }
1273
    }
1274
    return false;
1275
}
1276
 
1277
/**
1441 ariadna 1278
 * Process plugin callbacks for profile visibility.
1279
 *
1280
 * @param stdClass $user The user whose profile is being checked.
1281
 * @param stdClass|null $course The course context, if applicable.
1282
 * @param context|null $usercontext The user context, if applicable.
1283
 * @return int One of the core_user::VIEWPROFILE_* constants.
1284
 */
1285
function user_process_profile_callbacks(stdClass $user, ?stdClass $course = null, ?stdClass $usercontext = null): int {
1286
    $plugintypes = get_plugins_with_function('control_view_profile');
1287
    $forceallow = false;
1288
 
1289
    foreach ($plugintypes as $plugins) {
1290
        foreach ($plugins as $pluginfunction) {
1291
            $result = $pluginfunction($user, $course, $usercontext);
1292
            switch ($result) {
1293
                case core_user::VIEWPROFILE_FORCE_ALLOW:
1294
                    $forceallow = true;
1295
                    break;
1296
                case core_user::VIEWPROFILE_PREVENT:
1297
                    return core_user::VIEWPROFILE_PREVENT;
1298
            }
1299
        }
1300
    }
1301
 
1302
    return $forceallow ? core_user::VIEWPROFILE_FORCE_ALLOW : core_user::VIEWPROFILE_DO_NOT_PREVENT;
1303
}
1304
 
1305
/**
1 efrain 1306
 * Returns users tagged with a specified tag.
1307
 *
1308
 * @param core_tag_tag $tag
1309
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1310
 *             are displayed on the page and the per-page limit may be bigger
1311
 * @param int $fromctx context id where the link was displayed, may be used by callbacks
1312
 *            to display items in the same context first
1313
 * @param int $ctx context id where to search for records
1314
 * @param bool $rec search in subcontexts as well
1315
 * @param int $page 0-based number of page being displayed
1316
 * @return \core_tag\output\tagindex
1317
 */
1318
function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1319
    global $PAGE;
1320
 
1441 ariadna 1321
    $perpage = $exclusivemode ? 24 : 5;
1322
    $filteredusers = []; // Initialize an array to hold users that pass filtering.
1323
 
1324
    $totalusers = $tag->count_tagged_items('core', 'user', 'it.deleted=:notdeleted', ['notdeleted' => 0]);
1325
    $withinuserlimit = ($page * $perpage < $totalusers);
1326
    // Check if the requested page is within the user limit and if the context is valid or matches the system context.
1327
    if ($withinuserlimit && (!$ctx || $ctx == context_system::instance()->id)) {
1328
        // The output from get_tagged_items() will be filtered to check if users are visible to the current user.
1329
        // It’s possible that the count of users meeting the filtering criteria may fall short of the per-page limit,
1330
        // necessitating additional data beyond this limit.
1331
        // Implementing a batch approach addressed this issue by minimizing database queries.
1332
        $batch = 0;
1333
        // Increase the per-page limit to create a batch size for chunked querying.
1334
        // If the first chunk $perpagebatch doesn't return enough users, fetch the next chunk without re-querying the database.
1335
        $perpagebatch = $perpage * 2;
1336
 
1337
        do {
1338
            $userlist = $tag->get_tagged_items(
1339
                component: 'core',
1340
                itemtype: 'user',
1341
                limitfrom: $perpagebatch * $batch,
1342
                limitnum: $perpagebatch,
1343
                subquery: 'it.deleted=:notdeleted',
1344
                params: ['notdeleted' => 0],
1345
            );
1346
 
1347
            foreach ($userlist as $user) {
1348
                // Check if the user profile can be viewed.
1349
                if (user_can_view_profile($user)) {
1350
                    $filteredusers[] = $user;
1351
                    // If enough users have been collected for the requested page, exit both loops.
1352
                    if (count($filteredusers) > $perpage * ($page + 1)) {
1353
                        break 2;
1354
                    }
1355
                }
1356
            }
1357
 
1358
            $batch++;
1359
 
1360
        } while (count($userlist) > 0); // If all the data is still insufficient, run another batch.
1361
 
1 efrain 1362
    }
1441 ariadna 1363
 
1364
    $usercount = count($filteredusers);
1365
 
1366
    // Initialize the content to display tagged users.
1 efrain 1367
    $content = '';
1441 ariadna 1368
    if ($usercount > 0) {
1369
        // Prepare the paginated list of users, limiting it to the number of users per page.
1370
        $paginatedusers = array_slice($filteredusers, $page * $perpage, $perpage);
1 efrain 1371
 
1441 ariadna 1372
        // Get the renderer for the user module to create the user list content.
1 efrain 1373
        $renderer = $PAGE->get_renderer('core', 'user');
1441 ariadna 1374
        $content = $renderer->user_list($paginatedusers, $exclusivemode);
1 efrain 1375
    }
1376
 
1441 ariadna 1377
    // Calculate the total number of pages.
1378
    $totalpages = ceil($usercount / $perpage);
1379
 
1 efrain 1380
    return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1381
            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1382
}
1383
 
1384
/**
1385
 * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access a course.
1386
 *
1387
 * @param int $accesssince The unix timestamp to compare to users' last access
1388
 * @param string $tableprefix
1389
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1390
 * @return string
1391
 */
1392
function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul', $haveaccessed = false) {
1393
    return user_get_lastaccess_sql('timeaccess', $accesssince, $tableprefix, $haveaccessed);
1394
}
1395
 
1396
/**
1397
 * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access the system.
1398
 *
1399
 * @param int $accesssince The unix timestamp to compare to users' last access
1400
 * @param string $tableprefix
1401
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1402
 * @return string
1403
 */
1404
function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u', $haveaccessed = false) {
1405
    return user_get_lastaccess_sql('lastaccess', $accesssince, $tableprefix, $haveaccessed);
1406
}
1407
 
1408
/**
1409
 * Returns SQL that can be used to limit a query to a period where the user last accessed or
1410
 * did not access something recorded by a given table.
1411
 *
1412
 * @param string $columnname The name of the access column to check against
1413
 * @param int $accesssince The unix timestamp to compare to users' last access
1414
 * @param string $tableprefix The query prefix of the table to check
1415
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1416
 * @return string
1417
 */
1418
function user_get_lastaccess_sql($columnname, $accesssince, $tableprefix, $haveaccessed = false) {
1419
    if (empty($accesssince)) {
1420
        return '';
1421
    }
1422
 
1423
    // Only users who have accessed since $accesssince.
1424
    if ($haveaccessed) {
1425
        if ($accesssince == -1) {
1426
            // Include all users who have logged in at some point.
1427
            $sql = "({$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0)";
1428
        } else {
1429
            // Users who have accessed since the specified time.
1430
            $sql = "{$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0
1431
                AND {$tableprefix}.{$columnname} >= {$accesssince}";
1432
        }
1433
    } else {
1434
        // Only users who have not accessed since $accesssince.
1435
 
1436
        if ($accesssince == -1) {
1437
            // Users who have never accessed.
1438
            $sql = "({$tableprefix}.{$columnname} IS NULL OR {$tableprefix}.{$columnname} = 0)";
1439
        } else {
1440
            // Users who have not accessed since the specified time.
1441
            $sql = "({$tableprefix}.{$columnname} IS NULL
1442
                    OR ({$tableprefix}.{$columnname} != 0 AND {$tableprefix}.{$columnname} < {$accesssince}))";
1443
        }
1444
    }
1445
 
1446
    return $sql;
1447
}
1448
 
1449
/**
1450
 * Callback for inplace editable API.
1451
 *
1452
 * @param string $itemtype - Only user_roles is supported.
1453
 * @param string $itemid - Courseid and userid separated by a :
1454
 * @param string $newvalue - json encoded list of roleids.
1455
 * @return \core\output\inplace_editable|null
1456
 */
1457
function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
1458
    if ($itemtype === 'user_roles') {
1459
        return \core_user\output\user_roles_editable::update($itemid, $newvalue);
1460
    }
1461
}
1462
 
1463
/**
1464
 * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
1465
 *
1466
 * @param integer $userid
1467
 * @param string $fieldname
1468
 * @return string $purpose (empty string if there is no mapping).
1469
 */
1470
function user_edit_map_field_purpose($userid, $fieldname) {
1471
    global $USER;
1472
 
1473
    $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
1474
    // These are the fields considered valid to map and auto fill from a browser.
1475
    // We do not include fields that are in a collapsed section by default because
1476
    // the browser could auto-fill the field and cause a new value to be saved when
1477
    // that field was never visible.
1478
    $validmappings = array(
1479
        'username' => 'username',
1480
        'password' => 'current-password',
1481
        'firstname' => 'given-name',
1482
        'lastname' => 'family-name',
1483
        'middlename' => 'additional-name',
1484
        'email' => 'email',
1485
        'country' => 'country',
1486
        'lang' => 'language'
1487
    );
1488
 
1489
    $purpose = '';
1490
    // Only set a purpose when editing your own user details.
1491
    if ($currentuser && isset($validmappings[$fieldname])) {
1492
        $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
1493
    }
1494
 
1495
    return $purpose;
1496
}
1497
 
1498
/**
1499
 * Update the users public key for the specified device and app.
1500
 *
1501
 * @param string $uuid The device UUID.
1502
 * @param string $appid The app id, usually something like com.moodle.moodlemobile.
1503
 * @param string $publickey The app generated public key.
1504
 * @return bool
1505
 * @since Moodle 4.2
1506
 */
1507
function user_update_device_public_key(string $uuid, string $appid, string $publickey): bool {
1508
    global $USER, $DB;
1509
 
1510
    if (!$DB->get_record('user_devices',
1511
        ['uuid' => $uuid, 'appid' => $appid, 'userid' => $USER->id]
1512
    )) {
1513
        return false;
1514
    }
1515
 
1516
    $DB->set_field('user_devices', 'publickey', $publickey,
1517
        ['uuid' => $uuid, 'appid' => $appid, 'userid' => $USER->id]
1518
    );
1519
 
1520
    return true;
1521
}