Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// 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
        }
11 efrain 220
        if ($currentrecord->{$attributekey} !== $attributevalue) {
1 efrain 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
 
11 efrain 433
    if (!empty($user->address) && (in_array('address', $userfields) || $isadmin)) {
1 efrain 434
        $userdetails['address'] = $user->address;
435
    }
436
    if (!empty($user->phone1) && (in_array('phone1', $userfields)
437
            && in_array('phone1', $showuseridentityfields) || $isadmin)) {
438
        $userdetails['phone1'] = $user->phone1;
439
    }
440
    if (!empty($user->phone2) && (in_array('phone2', $userfields)
441
            && in_array('phone2', $showuseridentityfields) || $isadmin)) {
442
        $userdetails['phone2'] = $user->phone2;
443
    }
444
 
445
    if (isset($user->description) &&
446
        ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
447
        if (in_array('description', $userfields)) {
448
            // Always return the descriptionformat if description is requested.
449
            list($userdetails['description'], $userdetails['descriptionformat']) =
450
                    \core_external\util::format_text($user->description, $user->descriptionformat,
451
                            $usercontext, 'user', 'profile', null);
452
        }
453
    }
454
 
455
    if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
456
        $userdetails['country'] = $user->country;
457
    }
458
 
459
    if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
460
        $userdetails['city'] = $user->city;
461
    }
462
 
463
    if (in_array('timezone', $userfields) && (!isset($hiddenfields['timezone']) || $isadmin) && $user->timezone) {
464
        $userdetails['timezone'] = $user->timezone;
465
    }
466
 
467
    if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
468
        $userdetails['suspended'] = (bool)$user->suspended;
469
    }
470
 
471
    if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
472
        if ($user->firstaccess) {
473
            $userdetails['firstaccess'] = $user->firstaccess;
474
        } else {
475
            $userdetails['firstaccess'] = 0;
476
        }
477
    }
478
    if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
479
        if ($user->lastaccess) {
480
            $userdetails['lastaccess'] = $user->lastaccess;
481
        } else {
482
            $userdetails['lastaccess'] = 0;
483
        }
484
    }
485
 
486
    // Hidden fields restriction to lastaccess field applies to both site and course access time.
487
    if (in_array('lastcourseaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
488
        if (isset($user->lastcourseaccess)) {
489
            $userdetails['lastcourseaccess'] = $user->lastcourseaccess;
490
        } else {
491
            $userdetails['lastcourseaccess'] = 0;
492
        }
493
    }
494
 
495
    if (in_array('email', $userfields) && (
496
            $currentuser
497
            or (!isset($hiddenfields['email']) and (
498
                $user->maildisplay == core_user::MAILDISPLAY_EVERYONE
499
                or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER))
500
                or $canviewuseremail  // TODO: Deprecate/remove for MDL-37479.
501
            ))
502
            or in_array('email', $showuseridentityfields)
503
       )) {
504
        $userdetails['email'] = $user->email;
505
    }
506
 
507
    if (in_array('interests', $userfields)) {
508
        $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
509
        if ($interests) {
510
            $userdetails['interests'] = join(', ', $interests);
511
        }
512
    }
513
 
514
    // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
515
    if (in_array('idnumber', $userfields) && $user->idnumber) {
516
        if (in_array('idnumber', $showuseridentityfields) or $currentuser or
517
                has_capability('moodle/user:viewalldetails', $context)) {
518
            $userdetails['idnumber'] = $user->idnumber;
519
        }
520
    }
521
    if (in_array('institution', $userfields) && $user->institution) {
522
        if (in_array('institution', $showuseridentityfields) or $currentuser or
523
                has_capability('moodle/user:viewalldetails', $context)) {
524
            $userdetails['institution'] = $user->institution;
525
        }
526
    }
527
    // Isset because it's ok to have department 0.
528
    if (in_array('department', $userfields) && isset($user->department)) {
529
        if (in_array('department', $showuseridentityfields) or $currentuser or
530
                has_capability('moodle/user:viewalldetails', $context)) {
531
            $userdetails['department'] = $user->department;
532
        }
533
    }
534
 
535
    if (in_array('roles', $userfields) && !empty($course)) {
536
        // Not a big secret.
537
        $roles = get_user_roles($context, $user->id, false);
538
        $userdetails['roles'] = array();
539
        foreach ($roles as $role) {
540
            $userdetails['roles'][] = array(
541
                'roleid'       => $role->roleid,
542
                'name'         => $role->name,
543
                'shortname'    => $role->shortname,
544
                'sortorder'    => $role->sortorder
545
            );
546
        }
547
    }
548
 
549
    // Return user groups.
550
    if (in_array('groups', $userfields) && !empty($course)) {
551
        if ($usergroups = groups_get_all_groups($course->id, $user->id)) {
552
            $userdetails['groups'] = [];
553
            foreach ($usergroups as $group) {
554
                if ($course->groupmode == SEPARATEGROUPS && !$canaccessallgroups && $user->id != $USER->id) {
555
                    // In separate groups, I only have to see the groups shared between both users.
556
                    if (!groups_is_member($group->id, $USER->id)) {
557
                        continue;
558
                    }
559
                }
560
 
561
                $userdetails['groups'][] = [
562
                    'id' => $group->id,
563
                    'name' => format_string($group->name),
564
                    'description' => format_text($group->description, $group->descriptionformat, ['context' => $context]),
565
                    'descriptionformat' => $group->descriptionformat
566
                ];
567
            }
568
        }
569
    }
570
    // List of courses where the user is enrolled.
571
    if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
572
        $enrolledcourses = array();
573
        if ($mycourses = enrol_get_users_courses($user->id, true)) {
574
            foreach ($mycourses as $mycourse) {
575
                if ($mycourse->category) {
576
                    $coursecontext = context_course::instance($mycourse->id);
577
                    $enrolledcourse = array();
578
                    $enrolledcourse['id'] = $mycourse->id;
579
                    $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
580
                    $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
581
                    $enrolledcourses[] = $enrolledcourse;
582
                }
583
            }
584
            $userdetails['enrolledcourses'] = $enrolledcourses;
585
        }
586
    }
587
 
588
    // User preferences.
589
    if (in_array('preferences', $userfields) && $currentuser) {
590
        $preferences = array();
591
        $userpreferences = get_user_preferences();
592
        foreach ($userpreferences as $prefname => $prefvalue) {
593
            $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
594
        }
595
        $userdetails['preferences'] = $preferences;
596
    }
597
 
598
    if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
599
        $extrafields = ['auth', 'confirmed', 'lang', 'theme', 'mailformat', 'trackforums'];
600
        foreach ($extrafields as $extrafield) {
601
            if (in_array($extrafield, $userfields) && isset($user->$extrafield)) {
602
                $userdetails[$extrafield] = $user->$extrafield;
603
            }
604
        }
605
    }
606
 
607
    // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
608
    if (isset($userdetails['lang'])) {
609
        $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG);
610
    }
611
    if (isset($userdetails['theme'])) {
612
        $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME);
613
    }
614
 
615
    return $userdetails;
616
}
617
 
618
/**
619
 * Tries to obtain user details, either recurring directly to the user's system profile
620
 * or through one of the user's course enrollments (course profile).
621
 *
622
 * You can use the $userfields parameter to reduce the amount of a user record that is required by the method.
623
 * The minimum user fields are:
624
 *  * id
625
 *  * deleted
626
 *  * all potential fullname fields
627
 *
628
 * @param stdClass $user The user.
629
 * @param array $userfields An array of userfields to be returned, the values must be a
630
 *                          subset of user_get_default_fields (optional)
631
 * @return array if unsuccessful or the allowed user details.
632
 */
633
function user_get_user_details_courses($user, array $userfields = []) {
634
    global $USER;
635
    $userdetails = null;
636
 
637
    $systemprofile = false;
638
    if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
639
        $systemprofile = true;
640
    }
641
 
642
    // Try using system profile.
643
    if ($systemprofile) {
644
        $userdetails = user_get_user_details($user, null, $userfields);
645
    } else {
646
        // Try through course profile.
647
        // Get the courses that the user is enrolled in (only active).
648
        $courses = enrol_get_users_courses($user->id, true);
649
        foreach ($courses as $course) {
650
            if (user_can_view_profile($user, $course)) {
651
                $userdetails = user_get_user_details($user, $course, $userfields);
652
            }
653
        }
654
    }
655
 
656
    return $userdetails;
657
}
658
 
659
/**
660
 * Check if $USER have the necessary capabilities to obtain user details.
661
 *
662
 * @param stdClass $user
663
 * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
664
 * @return bool true if $USER can view user details.
665
 */
666
function can_view_user_details_cap($user, $course = null) {
667
    // Check $USER has the capability to view the user details at user context.
668
    $usercontext = context_user::instance($user->id);
669
    $result = has_capability('moodle/user:viewdetails', $usercontext);
670
    // Otherwise can $USER see them at course context.
671
    if (!$result && !empty($course)) {
672
        $context = context_course::instance($course->id);
673
        $result = has_capability('moodle/user:viewdetails', $context);
674
    }
675
    return $result;
676
}
677
 
678
/**
679
 * Return a list of page types
680
 * @param string $pagetype current page type
681
 * @param stdClass $parentcontext Block's parent context
682
 * @param stdClass $currentcontext Current context of block
683
 * @return array
684
 */
685
function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
686
    return array('user-profile' => get_string('page-user-profile', 'pagetype'));
687
}
688
 
689
/**
690
 * Count the number of failed login attempts for the given user, since last successful login.
691
 *
692
 * @param int|stdclass $user user id or object.
693
 * @param bool $reset Resets failed login count, if set to true.
694
 *
695
 * @return int number of failed login attempts since the last successful login.
696
 */
697
function user_count_login_failures($user, $reset = true) {
698
    global $DB;
699
 
700
    if (!is_object($user)) {
701
        $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
702
    }
703
    if ($user->deleted) {
704
        // Deleted user, nothing to do.
705
        return 0;
706
    }
707
    $count = get_user_preferences('login_failed_count_since_success', 0, $user);
708
    if ($reset) {
709
        set_user_preference('login_failed_count_since_success', 0, $user);
710
    }
711
    return $count;
712
}
713
 
714
/**
715
 * Converts a string into a flat array of menu items, where each menu items is a
716
 * stdClass with fields type, url, title.
717
 *
718
 * @param string $text the menu items definition
719
 * @param moodle_page $page the current page
720
 * @return array
721
 */
722
function user_convert_text_to_menu_items($text, $page) {
723
    $lines = explode("\n", $text);
724
    $children = array();
725
    foreach ($lines as $line) {
726
        $line = trim($line);
727
        $bits = explode('|', $line, 2);
728
        $itemtype = 'link';
729
        if (preg_match("/^#+$/", $line)) {
730
            $itemtype = 'divider';
731
        } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
732
            // Every item must have a name to be valid.
733
            continue;
734
        } else {
735
            $bits[0] = ltrim($bits[0], '-');
736
        }
737
 
738
        // Create the child.
739
        $child = new stdClass();
740
        $child->itemtype = $itemtype;
741
        if ($itemtype === 'divider') {
742
            // Add the divider to the list of children and skip link
743
            // processing.
744
            $children[] = $child;
745
            continue;
746
        }
747
 
748
        // Name processing.
749
        $namebits = explode(',', $bits[0], 2);
750
        if (count($namebits) == 2) {
751
            $namebits[1] = $namebits[1] ?: 'core';
752
            // Check the validity of the identifier part of the string.
753
            if (clean_param($namebits[0], PARAM_STRINGID) !== '' && clean_param($namebits[1], PARAM_COMPONENT) !== '') {
754
                // Treat this as a language string.
755
                $child->title = get_string($namebits[0], $namebits[1]);
756
                $child->titleidentifier = implode(',', $namebits);
757
            }
758
        }
759
        if (empty($child->title)) {
760
            // Use it as is, don't even clean it.
761
            $child->title = $bits[0];
762
            $child->titleidentifier = str_replace(" ", "-", $bits[0]);
763
        }
764
 
765
        // URL processing.
766
        if (!array_key_exists(1, $bits) or empty($bits[1])) {
767
            // Set the url to null, and set the itemtype to invalid.
768
            $bits[1] = null;
769
            $child->itemtype = "invalid";
770
        } else {
771
            // Nasty hack to replace the grades with the direct url.
772
            if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
773
                $bits[1] = user_mygrades_url();
774
            }
775
 
776
            // Make sure the url is a moodle url.
777
            $bits[1] = new moodle_url(trim($bits[1]));
778
        }
779
        $child->url = $bits[1];
780
 
781
        // Add this child to the list of children.
782
        $children[] = $child;
783
    }
784
    return $children;
785
}
786
 
787
/**
788
 * Get a list of essential user navigation items.
789
 *
790
 * @param stdclass $user user object.
791
 * @param moodle_page $page page object.
792
 * @param array $options associative array.
793
 *     options are:
794
 *     - avatarsize=35 (size of avatar image)
795
 * @return stdClass $returnobj navigation information object, where:
796
 *
797
 *      $returnobj->navitems    array    array of links where each link is a
798
 *                                       stdClass with fields url, title, and
799
 *                                       pix
800
 *      $returnobj->metadata    array    array of useful user metadata to be
801
 *                                       used when constructing navigation;
802
 *                                       fields include:
803
 *
804
 *          ROLE FIELDS
805
 *          asotherrole    bool    whether viewing as another role
806
 *          rolename       string  name of the role
807
 *
808
 *          USER FIELDS
809
 *          These fields are for the currently-logged in user, or for
810
 *          the user that the real user is currently logged in as.
811
 *
812
 *          userid         int        the id of the user in question
813
 *          userfullname   string     the user's full name
814
 *          userprofileurl moodle_url the url of the user's profile
815
 *          useravatar     string     a HTML fragment - the rendered
816
 *                                    user_picture for this user
817
 *          userloginfail  string     an error string denoting the number
818
 *                                    of login failures since last login
819
 *
820
 *          "REAL USER" FIELDS
821
 *          These fields are for when asotheruser is true, and
822
 *          correspond to the underlying "real user".
823
 *
824
 *          asotheruser        bool    whether viewing as another user
825
 *          realuserid         int        the id of the user in question
826
 *          realuserfullname   string     the user's full name
827
 *          realuserprofileurl moodle_url the url of the user's profile
828
 *          realuseravatar     string     a HTML fragment - the rendered
829
 *                                        user_picture for this user
830
 *
831
 *          MNET PROVIDER FIELDS
832
 *          asmnetuser            bool   whether viewing as a user from an
833
 *                                       MNet provider
834
 *          mnetidprovidername    string name of the MNet provider
835
 *          mnetidproviderwwwroot string URL of the MNet provider
836
 */
837
function user_get_user_navigation_info($user, $page, $options = array()) {
838
    global $OUTPUT, $DB, $SESSION, $CFG;
839
 
840
    $returnobject = new stdClass();
841
    $returnobject->navitems = array();
842
    $returnobject->metadata = array();
843
 
844
    $guest = isguestuser();
845
    if (!isloggedin() || $guest) {
846
        $returnobject->unauthenticateduser = [
847
            'guest' => $guest,
848
            'content' => $guest ? 'loggedinasguest' : 'loggedinnot',
849
        ];
850
 
851
        return $returnobject;
852
    }
853
 
854
    $course = $page->course;
855
 
856
    // Query the environment.
857
    $context = context_course::instance($course->id);
858
 
859
    // Get basic user metadata.
860
    $returnobject->metadata['userid'] = $user->id;
861
    $returnobject->metadata['userfullname'] = fullname($user);
862
    $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
863
        'id' => $user->id
864
    ));
865
 
866
    $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
867
    if (!empty($options['avatarsize'])) {
868
        $avataroptions['size'] = $options['avatarsize'];
869
    }
870
    $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
871
        $user, $avataroptions
872
    );
873
    // Build a list of items for a regular user.
874
 
875
    // Query MNet status.
876
    if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
877
        $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
878
        $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
879
        $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
880
    }
881
 
882
    // Did the user just log in?
883
    if (isset($SESSION->justloggedin)) {
884
        // Don't unset this flag as login_info still needs it.
885
        if (!empty($CFG->displayloginfailures)) {
886
            // Don't reset the count either, as login_info() still needs it too.
887
            if ($count = user_count_login_failures($user, false)) {
888
 
889
                // Get login failures string.
890
                $a = new stdClass();
891
                $a->attempts = html_writer::tag('span', $count, array('class' => 'value mr-1 font-weight-bold'));
892
                $returnobject->metadata['userloginfail'] =
893
                    get_string('failedloginattempts', '', $a);
894
 
895
            }
896
        }
897
    }
898
 
899
    $returnobject->metadata['asotherrole'] = false;
900
 
901
    // Before we add the last items (usually a logout + switch role link), add any
902
    // custom-defined items.
903
    $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
904
    $custommenucount = 0;
905
    foreach ($customitems as $item) {
906
        $returnobject->navitems[] = $item;
907
        if ($item->itemtype !== 'divider' && $item->itemtype !== 'invalid') {
908
            $custommenucount++;
909
        }
910
    }
911
 
912
    if ($custommenucount > 0) {
913
        // Only add a divider if we have customusermenuitems.
914
        $divider = new stdClass();
915
        $divider->itemtype = 'divider';
916
        $returnobject->navitems[] = $divider;
917
    }
918
 
919
    // Links: Preferences.
920
    $preferences = new stdClass();
921
    $preferences->itemtype = 'link';
922
    $preferences->url = new moodle_url('/user/preferences.php');
923
    $preferences->title = get_string('preferences');
924
    $preferences->titleidentifier = 'preferences,moodle';
925
    $returnobject->navitems[] = $preferences;
926
 
927
 
928
    if (is_role_switched($course->id)) {
929
        if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
930
            // Build role-return link instead of logout link.
931
            $rolereturn = new stdClass();
932
            $rolereturn->itemtype = 'link';
933
            $rolereturn->url = new moodle_url('/course/switchrole.php', array(
934
                'id' => $course->id,
935
                'sesskey' => sesskey(),
936
                'switchrole' => 0,
937
                'returnurl' => $page->url->out_as_local_url(false)
938
            ));
939
            $rolereturn->title = get_string('switchrolereturn');
940
            $rolereturn->titleidentifier = 'switchrolereturn,moodle';
941
            $returnobject->navitems[] = $rolereturn;
942
 
943
            $returnobject->metadata['asotherrole'] = true;
944
            $returnobject->metadata['rolename'] = role_get_name($role, $context);
945
 
946
        }
947
    } else {
948
        // Build switch role link.
949
        $roles = get_switchable_roles($context);
950
        if (is_array($roles) && (count($roles) > 0)) {
951
            $switchrole = new stdClass();
952
            $switchrole->itemtype = 'link';
953
            $switchrole->url = new moodle_url('/course/switchrole.php', array(
954
                'id' => $course->id,
955
                'switchrole' => -1,
956
                'returnurl' => $page->url->out_as_local_url(false)
957
            ));
958
            $switchrole->title = get_string('switchroleto');
959
            $switchrole->titleidentifier = 'switchroleto,moodle';
960
            $returnobject->navitems[] = $switchrole;
961
        }
962
    }
963
 
964
    if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
965
        $realuser = \core\session\manager::get_realuser();
966
 
967
        // Save values for the real user, as $user will be full of data for the
968
        // user is disguised as.
969
        $returnobject->metadata['realuserid'] = $realuser->id;
970
        $returnobject->metadata['realuserfullname'] = fullname($realuser);
971
        $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', [
972
            'id' => $realuser->id
973
        ]);
974
        $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
975
 
976
        // Build a user-revert link.
977
        $userrevert = new stdClass();
978
        $userrevert->itemtype = 'link';
979
        $userrevert->url = new moodle_url('/course/loginas.php', [
980
            'id' => $course->id,
981
            'sesskey' => sesskey()
982
        ]);
983
        $userrevert->title = get_string('logout');
984
        $userrevert->titleidentifier = 'logout,moodle';
985
        $returnobject->navitems[] = $userrevert;
986
    } else {
987
        // Build a logout link.
988
        $logout = new stdClass();
989
        $logout->itemtype = 'link';
990
        $logout->url = new moodle_url('/login/logout.php', ['sesskey' => sesskey()]);
991
        $logout->title = get_string('logout');
992
        $logout->titleidentifier = 'logout,moodle';
993
        $returnobject->navitems[] = $logout;
994
    }
995
 
996
    return $returnobject;
997
}
998
 
999
/**
1000
 * Add password to the list of used hashes for this user.
1001
 *
1002
 * This is supposed to be used from:
1003
 *  1/ change own password form
1004
 *  2/ password reset process
1005
 *  3/ user signup in auth plugins if password changing supported
1006
 *
1007
 * @param int $userid user id
1008
 * @param string $password plaintext password
1009
 * @return void
1010
 */
1011
function user_add_password_history(int $userid, #[\SensitiveParameter] string $password): void {
1012
    global $CFG, $DB;
1013
 
1014
    if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1015
        return;
1016
    }
1017
 
1018
    // Note: this is using separate code form normal password hashing because
1019
    // we need to have this under control in the future. Also, the auth
1020
    // plugin might not store the passwords locally at all.
1021
 
1022
    // First generate a cryptographically suitable salt.
1023
    $randombytes = random_bytes(16);
1024
    $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16);
1025
    // Then create the hash.
1026
    $generatedhash = crypt($password, '$6$rounds=10000$' . $salt . '$');
1027
 
1028
    $record = new stdClass();
1029
    $record->userid = $userid;
1030
    $record->hash = $generatedhash;
1031
    $record->timecreated = time();
1032
    $DB->insert_record('user_password_history', $record);
1033
 
1034
    $i = 0;
1035
    $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1036
    foreach ($records as $record) {
1037
        $i++;
1038
        if ($i > $CFG->passwordreuselimit) {
1039
            $DB->delete_records('user_password_history', array('id' => $record->id));
1040
        }
1041
    }
1042
}
1043
 
1044
/**
1045
 * Was this password used before on change or reset password page?
1046
 *
1047
 * The $CFG->passwordreuselimit setting determines
1048
 * how many times different password needs to be used
1049
 * before allowing previously used password again.
1050
 *
1051
 * @param int $userid user id
1052
 * @param string $password plaintext password
1053
 * @return bool true if password reused
1054
 */
1055
function user_is_previously_used_password($userid, $password) {
1056
    global $CFG, $DB;
1057
 
1058
    if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1059
        return false;
1060
    }
1061
 
1062
    $reused = false;
1063
 
1064
    $i = 0;
1065
    $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1066
    foreach ($records as $record) {
1067
        $i++;
1068
        if ($i > $CFG->passwordreuselimit) {
1069
            $DB->delete_records('user_password_history', array('id' => $record->id));
1070
            continue;
1071
        }
1072
        // NOTE: this is slow but we cannot compare the hashes directly any more.
1073
        if (password_verify($password, $record->hash)) {
1074
            $reused = true;
1075
        }
1076
    }
1077
 
1078
    return $reused;
1079
}
1080
 
1081
/**
1082
 * Remove a user device from the Moodle database (for PUSH notifications usually).
1083
 *
1084
 * @param string $uuid The device UUID.
1085
 * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1086
 * @return bool true if removed, false if the device didn't exists in the database
1087
 * @since Moodle 2.9
1088
 */
1089
function user_remove_user_device($uuid, $appid = "") {
1090
    global $DB, $USER;
1091
 
1092
    $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1093
    if (!empty($appid)) {
1094
        $conditions['appid'] = $appid;
1095
    }
1096
 
1097
    if (!$DB->count_records('user_devices', $conditions)) {
1098
        return false;
1099
    }
1100
 
1101
    $DB->delete_records('user_devices', $conditions);
1102
 
1103
    return true;
1104
}
1105
 
1106
/**
1107
 * Trigger user_list_viewed event.
1108
 *
1109
 * @param stdClass  $course course  object
1110
 * @param stdClass  $context course context object
1111
 * @since Moodle 2.9
1112
 */
1113
function user_list_view($course, $context) {
1114
 
1115
    $event = \core\event\user_list_viewed::create(array(
1116
        'objectid' => $course->id,
1117
        'courseid' => $course->id,
1118
        'context' => $context,
1119
        'other' => array(
1120
            'courseshortname' => $course->shortname,
1121
            'coursefullname' => $course->fullname
1122
        )
1123
    ));
1124
    $event->trigger();
1125
}
1126
 
1127
/**
1128
 * Returns the url to use for the "Grades" link in the user navigation.
1129
 *
1130
 * @param int $userid The user's ID.
1131
 * @param int $courseid The course ID if available.
1132
 * @return mixed A URL to be directed to for "Grades".
1133
 */
1134
function user_mygrades_url($userid = null, $courseid = SITEID) {
1135
    global $CFG, $USER;
1136
    $url = null;
1137
    if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1138
        if (isset($userid) && $USER->id != $userid) {
1139
            // Send to the gradebook report.
1140
            $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1141
                    array('id' => $courseid, 'userid' => $userid));
1142
        } else {
1143
            $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1144
        }
1145
    } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1146
            && !empty($CFG->gradereport_mygradeurl)) {
1147
        $url = $CFG->gradereport_mygradeurl;
1148
    } else {
1149
        $url = $CFG->wwwroot;
1150
    }
1151
    return $url;
1152
}
1153
 
1154
/**
1155
 * Check if the current user has permission to view details of the supplied user.
1156
 *
1157
 * This function supports two modes:
1158
 * If the optional $course param is omitted, then this function finds all shared courses and checks whether the current user has
1159
 * permission in any of them, returning true if so.
1160
 * If the $course param is provided, then this function checks permissions in ONLY that course.
1161
 *
1162
 * @param object $user The other user's details.
1163
 * @param object $course if provided, only check permissions in this course.
1164
 * @param context $usercontext The user context if available.
1165
 * @return bool true for ability to view this user, else false.
1166
 */
1167
function user_can_view_profile($user, $course = null, $usercontext = null) {
1168
    global $USER, $CFG;
1169
 
1170
    if ($user->deleted) {
1171
        return false;
1172
    }
1173
 
1174
    // Do we need to be logged in?
1175
    if (empty($CFG->forceloginforprofiles)) {
1176
        return true;
1177
    } else {
1178
       if (!isloggedin() || isguestuser()) {
1179
            // User is not logged in and forceloginforprofile is set, we need to return now.
1180
            return false;
1181
        }
1182
    }
1183
 
1184
    // Current user can always view their profile.
1185
    if ($USER->id == $user->id) {
1186
        return true;
1187
    }
1188
 
1189
    // Use callbacks so that (primarily) local plugins can prevent or allow profile access.
1190
    $forceallow = false;
1191
    $plugintypes = get_plugins_with_function('control_view_profile');
1192
    foreach ($plugintypes as $plugins) {
1193
        foreach ($plugins as $pluginfunction) {
1194
            $result = $pluginfunction($user, $course, $usercontext);
1195
            switch ($result) {
1196
                case core_user::VIEWPROFILE_DO_NOT_PREVENT:
1197
                    // If the plugin doesn't stop access, just continue to next plugin or use
1198
                    // default behaviour.
1199
                    break;
1200
                case core_user::VIEWPROFILE_FORCE_ALLOW:
1201
                    // Record that we are definitely going to allow it (unless another plugin
1202
                    // returns _PREVENT).
1203
                    $forceallow = true;
1204
                    break;
1205
                case core_user::VIEWPROFILE_PREVENT:
1206
                    // If any plugin returns PREVENT then we return false, regardless of what
1207
                    // other plugins said.
1208
                    return false;
1209
            }
1210
        }
1211
    }
1212
    if ($forceallow) {
1213
        return true;
1214
    }
1215
 
1216
    // Course contacts have visible profiles always.
1217
    if (has_coursecontact_role($user->id)) {
1218
        return true;
1219
    }
1220
 
1221
    // If we're only checking the capabilities in the single provided course.
1222
    if (isset($course)) {
1223
        // Confirm that $user is enrolled in the $course we're checking.
1224
        if (is_enrolled(context_course::instance($course->id), $user)) {
1225
            $userscourses = array($course);
1226
        }
1227
    } else {
1228
        // Else we're checking whether the current user can view $user's profile anywhere, so check user context first.
1229
        if (empty($usercontext)) {
1230
            $usercontext = context_user::instance($user->id);
1231
        }
1232
        if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) {
1233
            return true;
1234
        }
1235
        // This returns context information, so we can preload below.
1236
        $userscourses = enrol_get_all_users_courses($user->id);
1237
    }
1238
 
1239
    if (empty($userscourses)) {
1240
        return false;
1241
    }
1242
 
1243
    foreach ($userscourses as $userscourse) {
1244
        context_helper::preload_from_record($userscourse);
1245
        $coursecontext = context_course::instance($userscourse->id);
1246
        if (has_capability('moodle/user:viewdetails', $coursecontext) ||
1247
            has_capability('moodle/user:viewalldetails', $coursecontext)) {
1248
            if (!groups_user_groups_visible($userscourse, $user->id)) {
1249
                // Not a member of the same group.
1250
                continue;
1251
            }
1252
            return true;
1253
        }
1254
    }
1255
    return false;
1256
}
1257
 
1258
/**
1259
 * Returns users tagged with a specified tag.
1260
 *
1261
 * @param core_tag_tag $tag
1262
 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1263
 *             are displayed on the page and the per-page limit may be bigger
1264
 * @param int $fromctx context id where the link was displayed, may be used by callbacks
1265
 *            to display items in the same context first
1266
 * @param int $ctx context id where to search for records
1267
 * @param bool $rec search in subcontexts as well
1268
 * @param int $page 0-based number of page being displayed
1269
 * @return \core_tag\output\tagindex
1270
 */
1271
function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1272
    global $PAGE;
1273
 
1274
    if ($ctx && $ctx != context_system::instance()->id) {
1275
        $usercount = 0;
1276
    } else {
1277
        // Users can only be displayed in system context.
1278
        $usercount = $tag->count_tagged_items('core', 'user',
1279
                'it.deleted=:notdeleted', array('notdeleted' => 0));
1280
    }
1281
    $perpage = $exclusivemode ? 24 : 5;
1282
    $content = '';
1283
    $totalpages = ceil($usercount / $perpage);
1284
 
1285
    if ($usercount) {
1286
        $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage,
1287
                'it.deleted=:notdeleted', array('notdeleted' => 0));
1288
        $renderer = $PAGE->get_renderer('core', 'user');
1289
        $content .= $renderer->user_list($userlist, $exclusivemode);
1290
    }
1291
 
1292
    return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1293
            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1294
}
1295
 
1296
/**
1297
 * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access a course.
1298
 *
1299
 * @param int $accesssince The unix timestamp to compare to users' last access
1300
 * @param string $tableprefix
1301
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1302
 * @return string
1303
 */
1304
function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul', $haveaccessed = false) {
1305
    return user_get_lastaccess_sql('timeaccess', $accesssince, $tableprefix, $haveaccessed);
1306
}
1307
 
1308
/**
1309
 * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access the system.
1310
 *
1311
 * @param int $accesssince The unix timestamp to compare to users' last access
1312
 * @param string $tableprefix
1313
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1314
 * @return string
1315
 */
1316
function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u', $haveaccessed = false) {
1317
    return user_get_lastaccess_sql('lastaccess', $accesssince, $tableprefix, $haveaccessed);
1318
}
1319
 
1320
/**
1321
 * Returns SQL that can be used to limit a query to a period where the user last accessed or
1322
 * did not access something recorded by a given table.
1323
 *
1324
 * @param string $columnname The name of the access column to check against
1325
 * @param int $accesssince The unix timestamp to compare to users' last access
1326
 * @param string $tableprefix The query prefix of the table to check
1327
 * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1328
 * @return string
1329
 */
1330
function user_get_lastaccess_sql($columnname, $accesssince, $tableprefix, $haveaccessed = false) {
1331
    if (empty($accesssince)) {
1332
        return '';
1333
    }
1334
 
1335
    // Only users who have accessed since $accesssince.
1336
    if ($haveaccessed) {
1337
        if ($accesssince == -1) {
1338
            // Include all users who have logged in at some point.
1339
            $sql = "({$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0)";
1340
        } else {
1341
            // Users who have accessed since the specified time.
1342
            $sql = "{$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0
1343
                AND {$tableprefix}.{$columnname} >= {$accesssince}";
1344
        }
1345
    } else {
1346
        // Only users who have not accessed since $accesssince.
1347
 
1348
        if ($accesssince == -1) {
1349
            // Users who have never accessed.
1350
            $sql = "({$tableprefix}.{$columnname} IS NULL OR {$tableprefix}.{$columnname} = 0)";
1351
        } else {
1352
            // Users who have not accessed since the specified time.
1353
            $sql = "({$tableprefix}.{$columnname} IS NULL
1354
                    OR ({$tableprefix}.{$columnname} != 0 AND {$tableprefix}.{$columnname} < {$accesssince}))";
1355
        }
1356
    }
1357
 
1358
    return $sql;
1359
}
1360
 
1361
/**
1362
 * Callback for inplace editable API.
1363
 *
1364
 * @param string $itemtype - Only user_roles is supported.
1365
 * @param string $itemid - Courseid and userid separated by a :
1366
 * @param string $newvalue - json encoded list of roleids.
1367
 * @return \core\output\inplace_editable|null
1368
 */
1369
function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
1370
    if ($itemtype === 'user_roles') {
1371
        return \core_user\output\user_roles_editable::update($itemid, $newvalue);
1372
    }
1373
}
1374
 
1375
/**
1376
 * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
1377
 *
1378
 * @param integer $userid
1379
 * @param string $fieldname
1380
 * @return string $purpose (empty string if there is no mapping).
1381
 */
1382
function user_edit_map_field_purpose($userid, $fieldname) {
1383
    global $USER;
1384
 
1385
    $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
1386
    // These are the fields considered valid to map and auto fill from a browser.
1387
    // We do not include fields that are in a collapsed section by default because
1388
    // the browser could auto-fill the field and cause a new value to be saved when
1389
    // that field was never visible.
1390
    $validmappings = array(
1391
        'username' => 'username',
1392
        'password' => 'current-password',
1393
        'firstname' => 'given-name',
1394
        'lastname' => 'family-name',
1395
        'middlename' => 'additional-name',
1396
        'email' => 'email',
1397
        'country' => 'country',
1398
        'lang' => 'language'
1399
    );
1400
 
1401
    $purpose = '';
1402
    // Only set a purpose when editing your own user details.
1403
    if ($currentuser && isset($validmappings[$fieldname])) {
1404
        $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
1405
    }
1406
 
1407
    return $purpose;
1408
}
1409
 
1410
/**
1411
 * Update the users public key for the specified device and app.
1412
 *
1413
 * @param string $uuid The device UUID.
1414
 * @param string $appid The app id, usually something like com.moodle.moodlemobile.
1415
 * @param string $publickey The app generated public key.
1416
 * @return bool
1417
 * @since Moodle 4.2
1418
 */
1419
function user_update_device_public_key(string $uuid, string $appid, string $publickey): bool {
1420
    global $USER, $DB;
1421
 
1422
    if (!$DB->get_record('user_devices',
1423
        ['uuid' => $uuid, 'appid' => $appid, 'userid' => $USER->id]
1424
    )) {
1425
        return false;
1426
    }
1427
 
1428
    $DB->set_field('user_devices', 'publickey', $publickey,
1429
        ['uuid' => $uuid, 'appid' => $appid, 'userid' => $USER->id]
1430
    );
1431
 
1432
    return true;
1433
}