Proyectos de Subversion Moodle

Rev

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