e170a23c41ba31c7bae2bb41badb0258228759d3
[moodle.git] / user / lib.php
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/>.
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  */
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);
32 /**
33  * Creates a user
34  *
35  * @throws moodle_exception
36  * @param stdClass $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;
45     // Set the timecreate field to the current time.
46     if (!is_object($user)) {
47         $user = (object) $user;
48     }
50     // Check username.
51     if (trim($user->username) === '') {
52         throw new moodle_exception('invalidusernameblank');
53     }
55     if ($user->username !== core_text::strtolower($user->username)) {
56         throw new moodle_exception('usernamelowercase');
57     }
59     if ($user->username !== core_user::clean_field($user->username, 'username')) {
60         throw new moodle_exception('invalidusername');
61     }
63     // Save the password in a temp value for later.
64     if ($updatepassword && isset($user->password)) {
66         // Check password toward the password policy.
67         if (!check_password_policy($user->password, $errmsg)) {
68             throw new moodle_exception($errmsg);
69         }
71         $userpassword = $user->password;
72         unset($user->password);
73     }
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     }
98     $user->timecreated = time();
99     $user->timemodified = $user->timecreated;
101     // Validate user data object.
102     $uservalidation = core_user::validate($user);
103     if ($uservalidation !== true) {
104         foreach ($uservalidation as $field => $message) {
105             debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
106             $user->$field = core_user::clean_field($user->$field, $field);
107         }
108     }
110     // Insert the user into the database.
111     $newuserid = $DB->insert_record('user', $user);
113     // Create USER context for this user.
114     $usercontext = context_user::instance($newuserid);
116     // Update user password if necessary.
117     if (isset($userpassword)) {
118         // Get full database user row, in case auth is default.
119         $newuser = $DB->get_record('user', array('id' => $newuserid));
120         $authplugin = get_auth_plugin($newuser->auth);
121         $authplugin->user_update_password($newuser, $userpassword);
122     }
124     // Trigger event If required.
125     if ($triggerevent) {
126         \core\event\user_created::create_from_userid($newuserid)->trigger();
127     }
129     // Purge the associated caches.
130     cache_helper::purge_by_event('createduser');
132     return $newuserid;
135 /**
136  * Update a user with a user object (will compare against the ID)
137  *
138  * @throws moodle_exception
139  * @param stdClass $user the user to update
140  * @param bool $updatepassword if true, authentication plugin will update password.
141  * @param bool $triggerevent set false if user_updated event should not be triggred.
142  *             This will not affect user_password_updated event triggering.
143  */
144 function user_update_user($user, $updatepassword = true, $triggerevent = true) {
145     global $DB;
147     // Set the timecreate field to the current time.
148     if (!is_object($user)) {
149         $user = (object) $user;
150     }
152     // Check username.
153     if (isset($user->username)) {
154         if ($user->username !== core_text::strtolower($user->username)) {
155             throw new moodle_exception('usernamelowercase');
156         } else {
157             if ($user->username !== core_user::clean_field($user->username, 'username')) {
158                 throw new moodle_exception('invalidusername');
159             }
160         }
161     }
163     // Unset password here, for updating later, if password update is required.
164     if ($updatepassword && isset($user->password)) {
166         // Check password toward the password policy.
167         if (!check_password_policy($user->password, $errmsg)) {
168             throw new moodle_exception($errmsg);
169         }
171         $passwd = $user->password;
172         unset($user->password);
173     }
175     // Make sure calendartype, if set, is valid.
176     if (empty($user->calendartype)) {
177         // Unset this variable, must be an empty string, which we do not want to update the calendartype to.
178         unset($user->calendartype);
179     }
181     $user->timemodified = time();
183     // Validate user data object.
184     $uservalidation = core_user::validate($user);
185     if ($uservalidation !== true) {
186         foreach ($uservalidation as $field => $message) {
187             debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
188             $user->$field = core_user::clean_field($user->$field, $field);
189         }
190     }
192     $DB->update_record('user', $user);
194     if ($updatepassword) {
195         // Get full user record.
196         $updateduser = $DB->get_record('user', array('id' => $user->id));
198         // If password was set, then update its hash.
199         if (isset($passwd)) {
200             $authplugin = get_auth_plugin($updateduser->auth);
201             if ($authplugin->can_change_password()) {
202                 $authplugin->user_update_password($updateduser, $passwd);
203             }
204         }
205     }
206     // Trigger event if required.
207     if ($triggerevent) {
208         \core\event\user_updated::create_from_userid($user->id)->trigger();
209     }
212 /**
213  * Marks user deleted in internal user database and notifies the auth plugin.
214  * Also unenrols user from all roles and does other cleanup.
215  *
216  * @todo Decide if this transaction is really needed (look for internal TODO:)
217  * @param object $user Userobject before delete    (without system magic quotes)
218  * @return boolean success
219  */
220 function user_delete_user($user) {
221     return delete_user($user);
224 /**
225  * Get users by id
226  *
227  * @param array $userids id of users to retrieve
228  * @return array
229  */
230 function user_get_users_by_id($userids) {
231     global $DB;
232     return $DB->get_records_list('user', 'id', $userids);
235 /**
236  * Returns the list of default 'displayable' fields
237  *
238  * Contains database field names but also names used to generate information, such as enrolledcourses
239  *
240  * @return array of user fields
241  */
242 function user_get_default_fields() {
243     return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email',
244         'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo', 'aim', 'msn', 'department',
245         'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
246         'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
247         'city', 'url', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
248         'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended'
249     );
252 /**
253  *
254  * Give user record from mdl_user, build an array contains all user details.
255  *
256  * Warning: description file urls are 'webservice/pluginfile.php' is use.
257  *          it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
258  *
259  * @throws moodle_exception
260  * @param stdClass $user user record from mdl_user
261  * @param stdClass $course moodle course
262  * @param array $userfields required fields
263  * @return array|null
264  */
265 function user_get_user_details($user, $course = null, array $userfields = array()) {
266     global $USER, $DB, $CFG, $PAGE;
267     require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library.
268     require_once($CFG->dirroot . "/lib/filelib.php");      // File handling on description and friends.
270     $defaultfields = user_get_default_fields();
272     if (empty($userfields)) {
273         $userfields = $defaultfields;
274     }
276     foreach ($userfields as $thefield) {
277         if (!in_array($thefield, $defaultfields)) {
278             throw new moodle_exception('invaliduserfield', 'error', '', $thefield);
279         }
280     }
282     // Make sure id and fullname are included.
283     if (!in_array('id', $userfields)) {
284         $userfields[] = 'id';
285     }
287     if (!in_array('fullname', $userfields)) {
288         $userfields[] = 'fullname';
289     }
291     if (!empty($course)) {
292         $context = context_course::instance($course->id);
293         $usercontext = context_user::instance($user->id);
294         $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
295     } else {
296         $context = context_user::instance($user->id);
297         $usercontext = $context;
298         $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
299     }
301     $currentuser = ($user->id == $USER->id);
302     $isadmin = is_siteadmin($USER);
304     $showuseridentityfields = get_extra_user_fields($context);
306     if (!empty($course)) {
307         $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
308     } else {
309         $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
310     }
311     $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
312     if (!empty($course)) {
313         $canviewuseremail = has_capability('moodle/course:useremail', $context);
314     } else {
315         $canviewuseremail = false;
316     }
317     $cannotviewdescription   = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
318     if (!empty($course)) {
319         $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
320     } else {
321         $canaccessallgroups = false;
322     }
324     if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) {
325         // Skip this user details.
326         return null;
327     }
329     $userdetails = array();
330     $userdetails['id'] = $user->id;
332     if (in_array('username', $userfields)) {
333         if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
334             $userdetails['username'] = $user->username;
335         }
336     }
337     if ($isadmin or $canviewfullnames) {
338         if (in_array('firstname', $userfields)) {
339             $userdetails['firstname'] = $user->firstname;
340         }
341         if (in_array('lastname', $userfields)) {
342             $userdetails['lastname'] = $user->lastname;
343         }
344     }
345     $userdetails['fullname'] = fullname($user, $canviewfullnames);
347     if (in_array('customfields', $userfields)) {
348         $categories = profile_get_user_fields_with_data_by_category($user->id);
349         $userdetails['customfields'] = array();
350         foreach ($categories as $categoryid => $fields) {
351             foreach ($fields as $formfield) {
352                 if ($formfield->is_visible() and !$formfield->is_empty()) {
354                     // TODO: Part of MDL-50728, this conditional coding must be moved to
355                     // proper profile fields API so they are self-contained.
356                     // We only use display_data in fields that require text formatting.
357                     if ($formfield->field->datatype == 'text' or $formfield->field->datatype == 'textarea') {
358                         $fieldvalue = $formfield->display_data();
359                     } else {
360                         // Cases: datetime, checkbox and menu.
361                         $fieldvalue = $formfield->data;
362                     }
364                     $userdetails['customfields'][] =
365                         array('name' => $formfield->field->name, 'value' => $fieldvalue,
366                             'type' => $formfield->field->datatype, 'shortname' => $formfield->field->shortname);
367                 }
368             }
369         }
370         // Unset customfields if it's empty.
371         if (empty($userdetails['customfields'])) {
372             unset($userdetails['customfields']);
373         }
374     }
376     // Profile image.
377     if (in_array('profileimageurl', $userfields)) {
378         $userpicture = new user_picture($user);
379         $userpicture->size = 1; // Size f1.
380         $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
381     }
382     if (in_array('profileimageurlsmall', $userfields)) {
383         if (!isset($userpicture)) {
384             $userpicture = new user_picture($user);
385         }
386         $userpicture->size = 0; // Size f2.
387         $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
388     }
390     // Hidden user field.
391     if ($canviewhiddenuserfields) {
392         $hiddenfields = array();
393         // Address, phone1 and phone2 not appears in hidden fields list but require viewhiddenfields capability
394         // according to user/profile.php.
395         if (!empty($user->address) && in_array('address', $userfields)) {
396             $userdetails['address'] = $user->address;
397         }
398     } else {
399         $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
400     }
402     if (!empty($user->phone1) && in_array('phone1', $userfields) &&
403             (in_array('phone1', $showuseridentityfields) or $canviewhiddenuserfields)) {
404         $userdetails['phone1'] = $user->phone1;
405     }
406     if (!empty($user->phone2) && in_array('phone2', $userfields) &&
407             (in_array('phone2', $showuseridentityfields) or $canviewhiddenuserfields)) {
408         $userdetails['phone2'] = $user->phone2;
409     }
411     if (isset($user->description) &&
412         ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
413         if (in_array('description', $userfields)) {
414             // Always return the descriptionformat if description is requested.
415             list($userdetails['description'], $userdetails['descriptionformat']) =
416                     external_format_text($user->description, $user->descriptionformat,
417                             $usercontext->id, 'user', 'profile', null);
418         }
419     }
421     if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
422         $userdetails['country'] = $user->country;
423     }
425     if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
426         $userdetails['city'] = $user->city;
427     }
429     if (in_array('url', $userfields) && $user->url && (!isset($hiddenfields['webpage']) or $isadmin)) {
430         $url = $user->url;
431         if (strpos($user->url, '://') === false) {
432             $url = 'http://'. $url;
433         }
434         $user->url = clean_param($user->url, PARAM_URL);
435         $userdetails['url'] = $user->url;
436     }
438     if (in_array('icq', $userfields) && $user->icq && (!isset($hiddenfields['icqnumber']) or $isadmin)) {
439         $userdetails['icq'] = $user->icq;
440     }
442     if (in_array('skype', $userfields) && $user->skype && (!isset($hiddenfields['skypeid']) or $isadmin)) {
443         $userdetails['skype'] = $user->skype;
444     }
445     if (in_array('yahoo', $userfields) && $user->yahoo && (!isset($hiddenfields['yahooid']) or $isadmin)) {
446         $userdetails['yahoo'] = $user->yahoo;
447     }
448     if (in_array('aim', $userfields) && $user->aim && (!isset($hiddenfields['aimid']) or $isadmin)) {
449         $userdetails['aim'] = $user->aim;
450     }
451     if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
452         $userdetails['msn'] = $user->msn;
453     }
454     if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
455         $userdetails['suspended'] = (bool)$user->suspended;
456     }
458     if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
459         if ($user->firstaccess) {
460             $userdetails['firstaccess'] = $user->firstaccess;
461         } else {
462             $userdetails['firstaccess'] = 0;
463         }
464     }
465     if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
466         if ($user->lastaccess) {
467             $userdetails['lastaccess'] = $user->lastaccess;
468         } else {
469             $userdetails['lastaccess'] = 0;
470         }
471     }
473     if (in_array('email', $userfields) && (
474             $currentuser
475             or (!isset($hiddenfields['email']) and (
476                 $user->maildisplay == core_user::MAILDISPLAY_EVERYONE
477                 or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER))
478                 or $canviewuseremail  // TODO: Deprecate/remove for MDL-37479.
479             ))
480             or in_array('email', $showuseridentityfields)
481        )) {
482         $userdetails['email'] = $user->email;
483     }
485     if (in_array('interests', $userfields)) {
486         $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
487         if ($interests) {
488             $userdetails['interests'] = join(', ', $interests);
489         }
490     }
492     // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
493     if (in_array('idnumber', $userfields) && $user->idnumber) {
494         if (in_array('idnumber', $showuseridentityfields) or $currentuser or
495                 has_capability('moodle/user:viewalldetails', $context)) {
496             $userdetails['idnumber'] = $user->idnumber;
497         }
498     }
499     if (in_array('institution', $userfields) && $user->institution) {
500         if (in_array('institution', $showuseridentityfields) or $currentuser or
501                 has_capability('moodle/user:viewalldetails', $context)) {
502             $userdetails['institution'] = $user->institution;
503         }
504     }
505     // Isset because it's ok to have department 0.
506     if (in_array('department', $userfields) && isset($user->department)) {
507         if (in_array('department', $showuseridentityfields) or $currentuser or
508                 has_capability('moodle/user:viewalldetails', $context)) {
509             $userdetails['department'] = $user->department;
510         }
511     }
513     if (in_array('roles', $userfields) && !empty($course)) {
514         // Not a big secret.
515         $roles = get_user_roles($context, $user->id, false);
516         $userdetails['roles'] = array();
517         foreach ($roles as $role) {
518             $userdetails['roles'][] = array(
519                 'roleid'       => $role->roleid,
520                 'name'         => $role->name,
521                 'shortname'    => $role->shortname,
522                 'sortorder'    => $role->sortorder
523             );
524         }
525     }
527     // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
528     if (in_array('groups', $userfields) && !empty($course) && $canaccessallgroups) {
529         $usergroups = groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid,
530                 'g.id, g.name,g.description,g.descriptionformat');
531         $userdetails['groups'] = array();
532         foreach ($usergroups as $group) {
533             list($group->description, $group->descriptionformat) =
534                 external_format_text($group->description, $group->descriptionformat,
535                         $context->id, 'group', 'description', $group->id);
536             $userdetails['groups'][] = array('id' => $group->id, 'name' => $group->name,
537                 'description' => $group->description, 'descriptionformat' => $group->descriptionformat);
538         }
539     }
540     // List of courses where the user is enrolled.
541     if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
542         $enrolledcourses = array();
543         if ($mycourses = enrol_get_users_courses($user->id, true)) {
544             foreach ($mycourses as $mycourse) {
545                 if ($mycourse->category) {
546                     $coursecontext = context_course::instance($mycourse->id);
547                     $enrolledcourse = array();
548                     $enrolledcourse['id'] = $mycourse->id;
549                     $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
550                     $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
551                     $enrolledcourses[] = $enrolledcourse;
552                 }
553             }
554             $userdetails['enrolledcourses'] = $enrolledcourses;
555         }
556     }
558     // User preferences.
559     if (in_array('preferences', $userfields) && $currentuser) {
560         $preferences = array();
561         $userpreferences = get_user_preferences();
562         foreach ($userpreferences as $prefname => $prefvalue) {
563             $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
564         }
565         $userdetails['preferences'] = $preferences;
566     }
568     if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
569         $extrafields = ['auth', 'confirmed', 'lang', 'theme', 'timezone', 'mailformat'];
570         foreach ($extrafields as $extrafield) {
571             if (in_array($extrafield, $userfields) && isset($user->$extrafield)) {
572                 $userdetails[$extrafield] = $user->$extrafield;
573             }
574         }
575     }
577     // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
578     if (isset($userdetails['lang'])) {
579         $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG);
580     }
581     if (isset($userdetails['theme'])) {
582         $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME);
583     }
585     return $userdetails;
588 /**
589  * Tries to obtain user details, either recurring directly to the user's system profile
590  * or through one of the user's course enrollments (course profile).
591  *
592  * @param stdClass $user The user.
593  * @return array if unsuccessful or the allowed user details.
594  */
595 function user_get_user_details_courses($user) {
596     global $USER;
597     $userdetails = null;
599     // Get the courses that the user is enrolled in (only active).
600     $courses = enrol_get_users_courses($user->id, true);
602     $systemprofile = false;
603     if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
604         $systemprofile = true;
605     }
607     // Try using system profile.
608     if ($systemprofile) {
609         $userdetails = user_get_user_details($user, null);
610     } else {
611         // Try through course profile.
612         foreach ($courses as $course) {
613             if (can_view_user_details_cap($user, $course) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
614                 $userdetails = user_get_user_details($user, $course);
615             }
616         }
617     }
619     return $userdetails;
622 /**
623  * Check if $USER have the necessary capabilities to obtain user details.
624  *
625  * @param stdClass $user
626  * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
627  * @return bool true if $USER can view user details.
628  */
629 function can_view_user_details_cap($user, $course = null) {
630     // Check $USER has the capability to view the user details at user context.
631     $usercontext = context_user::instance($user->id);
632     $result = has_capability('moodle/user:viewdetails', $usercontext);
633     // Otherwise can $USER see them at course context.
634     if (!$result && !empty($course)) {
635         $context = context_course::instance($course->id);
636         $result = has_capability('moodle/user:viewdetails', $context);
637     }
638     return $result;
641 /**
642  * Return a list of page types
643  * @param string $pagetype current page type
644  * @param stdClass $parentcontext Block's parent context
645  * @param stdClass $currentcontext Current context of block
646  * @return array
647  */
648 function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
649     return array('user-profile' => get_string('page-user-profile', 'pagetype'));
652 /**
653  * Count the number of failed login attempts for the given user, since last successful login.
654  *
655  * @param int|stdclass $user user id or object.
656  * @param bool $reset Resets failed login count, if set to true.
657  *
658  * @return int number of failed login attempts since the last successful login.
659  */
660 function user_count_login_failures($user, $reset = true) {
661     global $DB;
663     if (!is_object($user)) {
664         $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
665     }
666     if ($user->deleted) {
667         // Deleted user, nothing to do.
668         return 0;
669     }
670     $count = get_user_preferences('login_failed_count_since_success', 0, $user);
671     if ($reset) {
672         set_user_preference('login_failed_count_since_success', 0, $user);
673     }
674     return $count;
677 /**
678  * Converts a string into a flat array of menu items, where each menu items is a
679  * stdClass with fields type, url, title, pix, and imgsrc.
680  *
681  * @param string $text the menu items definition
682  * @param moodle_page $page the current page
683  * @return array
684  */
685 function user_convert_text_to_menu_items($text, $page) {
686     global $OUTPUT, $CFG;
688     $lines = explode("\n", $text);
689     $items = array();
690     $lastchild = null;
691     $lastdepth = null;
692     $lastsort = 0;
693     $children = array();
694     foreach ($lines as $line) {
695         $line = trim($line);
696         $bits = explode('|', $line, 3);
697         $itemtype = 'link';
698         if (preg_match("/^#+$/", $line)) {
699             $itemtype = 'divider';
700         } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
701             // Every item must have a name to be valid.
702             continue;
703         } else {
704             $bits[0] = ltrim($bits[0], '-');
705         }
707         // Create the child.
708         $child = new stdClass();
709         $child->itemtype = $itemtype;
710         if ($itemtype === 'divider') {
711             // Add the divider to the list of children and skip link
712             // processing.
713             $children[] = $child;
714             continue;
715         }
717         // Name processing.
718         $namebits = explode(',', $bits[0], 2);
719         if (count($namebits) == 2) {
720             // Check the validity of the identifier part of the string.
721             if (clean_param($namebits[0], PARAM_STRINGID) !== '') {
722                 // Treat this as a language string.
723                 $child->title = get_string($namebits[0], $namebits[1]);
724                 $child->titleidentifier = implode(',', $namebits);
725             }
726         }
727         if (empty($child->title)) {
728             // Use it as is, don't even clean it.
729             $child->title = $bits[0];
730             $child->titleidentifier = str_replace(" ", "-", $bits[0]);
731         }
733         // URL processing.
734         if (!array_key_exists(1, $bits) or empty($bits[1])) {
735             // Set the url to null, and set the itemtype to invalid.
736             $bits[1] = null;
737             $child->itemtype = "invalid";
738         } else {
739             // Nasty hack to replace the grades with the direct url.
740             if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
741                 $bits[1] = user_mygrades_url();
742             }
744             // Make sure the url is a moodle url.
745             $bits[1] = new moodle_url(trim($bits[1]));
746         }
747         $child->url = $bits[1];
749         // PIX processing.
750         $pixpath = "t/edit";
751         if (!array_key_exists(2, $bits) or empty($bits[2])) {
752             // Use the default.
753             $child->pix = $pixpath;
754         } else {
755             // Check for the specified image existing.
756             if (strpos($bits[2], '../') === 0) {
757                 // The string starts with '../'.
758                 // Strip off the first three characters - this should be the pix path.
759                 $pixpath = substr($bits[2], 3);
760             } else if (strpos($bits[2], '/') === false) {
761                 // There is no / in the path. Prefix it with 't/', which is the default path.
762                 $pixpath = "t/{$bits[2]}";
763             } else {
764                 // There is a '/' in the path - this is either a URL, or a standard pix path with no changes required.
765                 $pixpath = $bits[2];
766             }
767             if ($page->theme->resolve_image_location($pixpath, 'moodle', true)) {
768                 // Use the image.
769                 $child->pix = $pixpath;
770             } else {
771                 // Treat it like a URL.
772                 $child->pix = null;
773                 $child->imgsrc = $bits[2];
774             }
775         }
777         // Add this child to the list of children.
778         $children[] = $child;
779     }
780     return $children;
783 /**
784  * Get a list of essential user navigation items.
785  *
786  * @param stdclass $user user object.
787  * @param moodle_page $page page object.
788  * @param array $options associative array.
789  *     options are:
790  *     - avatarsize=35 (size of avatar image)
791  * @return stdClass $returnobj navigation information object, where:
792  *
793  *      $returnobj->navitems    array    array of links where each link is a
794  *                                       stdClass with fields url, title, and
795  *                                       pix
796  *      $returnobj->metadata    array    array of useful user metadata to be
797  *                                       used when constructing navigation;
798  *                                       fields include:
799  *
800  *          ROLE FIELDS
801  *          asotherrole    bool    whether viewing as another role
802  *          rolename       string  name of the role
803  *
804  *          USER FIELDS
805  *          These fields are for the currently-logged in user, or for
806  *          the user that the real user is currently logged in as.
807  *
808  *          userid         int        the id of the user in question
809  *          userfullname   string     the user's full name
810  *          userprofileurl moodle_url the url of the user's profile
811  *          useravatar     string     a HTML fragment - the rendered
812  *                                    user_picture for this user
813  *          userloginfail  string     an error string denoting the number
814  *                                    of login failures since last login
815  *
816  *          "REAL USER" FIELDS
817  *          These fields are for when asotheruser is true, and
818  *          correspond to the underlying "real user".
819  *
820  *          asotheruser        bool    whether viewing as another user
821  *          realuserid         int        the id of the user in question
822  *          realuserfullname   string     the user's full name
823  *          realuserprofileurl moodle_url the url of the user's profile
824  *          realuseravatar     string     a HTML fragment - the rendered
825  *                                        user_picture for this user
826  *
827  *          MNET PROVIDER FIELDS
828  *          asmnetuser            bool   whether viewing as a user from an
829  *                                       MNet provider
830  *          mnetidprovidername    string name of the MNet provider
831  *          mnetidproviderwwwroot string URL of the MNet provider
832  */
833 function user_get_user_navigation_info($user, $page, $options = array()) {
834     global $OUTPUT, $DB, $SESSION, $CFG;
836     $returnobject = new stdClass();
837     $returnobject->navitems = array();
838     $returnobject->metadata = array();
840     $course = $page->course;
842     // Query the environment.
843     $context = context_course::instance($course->id);
845     // Get basic user metadata.
846     $returnobject->metadata['userid'] = $user->id;
847     $returnobject->metadata['userfullname'] = fullname($user, true);
848     $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
849         'id' => $user->id
850     ));
852     $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
853     if (!empty($options['avatarsize'])) {
854         $avataroptions['size'] = $options['avatarsize'];
855     }
856     $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
857         $user, $avataroptions
858     );
859     // Build a list of items for a regular user.
861     // Query MNet status.
862     if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
863         $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
864         $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
865         $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
866     }
868     // Did the user just log in?
869     if (isset($SESSION->justloggedin)) {
870         // Don't unset this flag as login_info still needs it.
871         if (!empty($CFG->displayloginfailures)) {
872             // Don't reset the count either, as login_info() still needs it too.
873             if ($count = user_count_login_failures($user, false)) {
875                 // Get login failures string.
876                 $a = new stdClass();
877                 $a->attempts = html_writer::tag('span', $count, array('class' => 'value'));
878                 $returnobject->metadata['userloginfail'] =
879                     get_string('failedloginattempts', '', $a);
881             }
882         }
883     }
885     // Links: Dashboard.
886     $myhome = new stdClass();
887     $myhome->itemtype = 'link';
888     $myhome->url = new moodle_url('/my/');
889     $myhome->title = get_string('mymoodle', 'admin');
890     $myhome->titleidentifier = 'mymoodle,admin';
891     $myhome->pix = "i/dashboard";
892     $returnobject->navitems[] = $myhome;
894     // Links: My Profile.
895     $myprofile = new stdClass();
896     $myprofile->itemtype = 'link';
897     $myprofile->url = new moodle_url('/user/profile.php', array('id' => $user->id));
898     $myprofile->title = get_string('profile');
899     $myprofile->titleidentifier = 'profile,moodle';
900     $myprofile->pix = "i/user";
901     $returnobject->navitems[] = $myprofile;
903     $returnobject->metadata['asotherrole'] = false;
905     // Before we add the last items (usually a logout + switch role link), add any
906     // custom-defined items.
907     $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
908     foreach ($customitems as $item) {
909         $returnobject->navitems[] = $item;
910     }
913     if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
914         $realuser = \core\session\manager::get_realuser();
916         // Save values for the real user, as $user will be full of data for the
917         // user the user is disguised as.
918         $returnobject->metadata['realuserid'] = $realuser->id;
919         $returnobject->metadata['realuserfullname'] = fullname($realuser, true);
920         $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', array(
921             'id' => $realuser->id
922         ));
923         $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
925         // Build a user-revert link.
926         $userrevert = new stdClass();
927         $userrevert->itemtype = 'link';
928         $userrevert->url = new moodle_url('/course/loginas.php', array(
929             'id' => $course->id,
930             'sesskey' => sesskey()
931         ));
932         $userrevert->pix = "a/logout";
933         $userrevert->title = get_string('logout');
934         $userrevert->titleidentifier = 'logout,moodle';
935         $returnobject->navitems[] = $userrevert;
937     } else {
939         // Build a logout link.
940         $logout = new stdClass();
941         $logout->itemtype = 'link';
942         $logout->url = new moodle_url('/login/logout.php', array('sesskey' => sesskey()));
943         $logout->pix = "a/logout";
944         $logout->title = get_string('logout');
945         $logout->titleidentifier = 'logout,moodle';
946         $returnobject->navitems[] = $logout;
947     }
949     if (is_role_switched($course->id)) {
950         if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
951             // Build role-return link instead of logout link.
952             $rolereturn = new stdClass();
953             $rolereturn->itemtype = 'link';
954             $rolereturn->url = new moodle_url('/course/switchrole.php', array(
955                 'id' => $course->id,
956                 'sesskey' => sesskey(),
957                 'switchrole' => 0,
958                 'returnurl' => $page->url->out_as_local_url(false)
959             ));
960             $rolereturn->pix = "a/logout";
961             $rolereturn->title = get_string('switchrolereturn');
962             $rolereturn->titleidentifier = 'switchrolereturn,moodle';
963             $returnobject->navitems[] = $rolereturn;
965             $returnobject->metadata['asotherrole'] = true;
966             $returnobject->metadata['rolename'] = role_get_name($role, $context);
968         }
969     } else {
970         // Build switch role link.
971         $roles = get_switchable_roles($context);
972         if (is_array($roles) && (count($roles) > 0)) {
973             $switchrole = new stdClass();
974             $switchrole->itemtype = 'link';
975             $switchrole->url = new moodle_url('/course/switchrole.php', array(
976                 'id' => $course->id,
977                 'switchrole' => -1,
978                 'returnurl' => $page->url->out_as_local_url(false)
979             ));
980             $switchrole->pix = "i/switchrole";
981             $switchrole->title = get_string('switchroleto');
982             $switchrole->titleidentifier = 'switchroleto,moodle';
983             $returnobject->navitems[] = $switchrole;
984         }
985     }
987     return $returnobject;
990 /**
991  * Add password to the list of used hashes for this user.
992  *
993  * This is supposed to be used from:
994  *  1/ change own password form
995  *  2/ password reset process
996  *  3/ user signup in auth plugins if password changing supported
997  *
998  * @param int $userid user id
999  * @param string $password plaintext password
1000  * @return void
1001  */
1002 function user_add_password_history($userid, $password) {
1003     global $CFG, $DB;
1005     if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1006         return;
1007     }
1009     // Note: this is using separate code form normal password hashing because
1010     //       we need to have this under control in the future. Also the auth
1011     //       plugin might not store the passwords locally at all.
1013     $record = new stdClass();
1014     $record->userid = $userid;
1015     $record->hash = password_hash($password, PASSWORD_DEFAULT);
1016     $record->timecreated = time();
1017     $DB->insert_record('user_password_history', $record);
1019     $i = 0;
1020     $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1021     foreach ($records as $record) {
1022         $i++;
1023         if ($i > $CFG->passwordreuselimit) {
1024             $DB->delete_records('user_password_history', array('id' => $record->id));
1025         }
1026     }
1029 /**
1030  * Was this password used before on change or reset password page?
1031  *
1032  * The $CFG->passwordreuselimit setting determines
1033  * how many times different password needs to be used
1034  * before allowing previously used password again.
1035  *
1036  * @param int $userid user id
1037  * @param string $password plaintext password
1038  * @return bool true if password reused
1039  */
1040 function user_is_previously_used_password($userid, $password) {
1041     global $CFG, $DB;
1043     if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1044         return false;
1045     }
1047     $reused = false;
1049     $i = 0;
1050     $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1051     foreach ($records as $record) {
1052         $i++;
1053         if ($i > $CFG->passwordreuselimit) {
1054             $DB->delete_records('user_password_history', array('id' => $record->id));
1055             continue;
1056         }
1057         // NOTE: this is slow but we cannot compare the hashes directly any more.
1058         if (password_verify($password, $record->hash)) {
1059             $reused = true;
1060         }
1061     }
1063     return $reused;
1066 /**
1067  * Remove a user device from the Moodle database (for PUSH notifications usually).
1068  *
1069  * @param string $uuid The device UUID.
1070  * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1071  * @return bool true if removed, false if the device didn't exists in the database
1072  * @since Moodle 2.9
1073  */
1074 function user_remove_user_device($uuid, $appid = "") {
1075     global $DB, $USER;
1077     $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1078     if (!empty($appid)) {
1079         $conditions['appid'] = $appid;
1080     }
1082     if (!$DB->count_records('user_devices', $conditions)) {
1083         return false;
1084     }
1086     $DB->delete_records('user_devices', $conditions);
1088     return true;
1091 /**
1092  * Trigger user_list_viewed event.
1093  *
1094  * @param stdClass  $course course  object
1095  * @param stdClass  $context course context object
1096  * @since Moodle 2.9
1097  */
1098 function user_list_view($course, $context) {
1100     $event = \core\event\user_list_viewed::create(array(
1101         'objectid' => $course->id,
1102         'courseid' => $course->id,
1103         'context' => $context,
1104         'other' => array(
1105             'courseshortname' => $course->shortname,
1106             'coursefullname' => $course->fullname
1107         )
1108     ));
1109     $event->trigger();
1112 /**
1113  * Returns the url to use for the "Grades" link in the user navigation.
1114  *
1115  * @param int $userid The user's ID.
1116  * @param int $courseid The course ID if available.
1117  * @return mixed A URL to be directed to for "Grades".
1118  */
1119 function user_mygrades_url($userid = null, $courseid = SITEID) {
1120     global $CFG, $USER;
1121     $url = null;
1122     if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1123         if (isset($userid) && $USER->id != $userid) {
1124             // Send to the gradebook report.
1125             $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1126                     array('id' => $courseid, 'userid' => $userid));
1127         } else {
1128             $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1129         }
1130     } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1131             && !empty($CFG->gradereport_mygradeurl)) {
1132         $url = $CFG->gradereport_mygradeurl;
1133     } else {
1134         $url = $CFG->wwwroot;
1135     }
1136     return $url;
1139 /**
1140  * Check if the current user has permission to view details of the supplied user.
1141  *
1142  * This function supports two modes:
1143  * If the optional $course param is omitted, then this function finds all shared courses and checks whether the current user has
1144  * permission in any of them, returning true if so.
1145  * If the $course param is provided, then this function checks permissions in ONLY that course.
1146  *
1147  * @param object $user The other user's details.
1148  * @param object $course if provided, only check permissions in this course.
1149  * @param context $usercontext The user context if available.
1150  * @return bool true for ability to view this user, else false.
1151  */
1152 function user_can_view_profile($user, $course = null, $usercontext = null) {
1153     global $USER, $CFG;
1155     if ($user->deleted) {
1156         return false;
1157     }
1159     // Do we need to be logged in?
1160     if (empty($CFG->forceloginforprofiles)) {
1161         return true;
1162     } else {
1163        if (!isloggedin() || isguestuser()) {
1164             // User is not logged in and forceloginforprofile is set, we need to return now.
1165             return false;
1166         }
1167     }
1169     // Current user can always view their profile.
1170     if ($USER->id == $user->id) {
1171         return true;
1172     }
1174     // Use callbacks so that (primarily) local plugins can prevent or allow profile access.
1175     $forceallow = false;
1176     $plugintypes = get_plugins_with_function('control_view_profile');
1177     foreach ($plugintypes as $plugins) {
1178         foreach ($plugins as $pluginfunction) {
1179             $result = $pluginfunction($user, $course, $usercontext);
1180             switch ($result) {
1181                 case core_user::VIEWPROFILE_DO_NOT_PREVENT:
1182                     // If the plugin doesn't stop access, just continue to next plugin or use
1183                     // default behaviour.
1184                     break;
1185                 case core_user::VIEWPROFILE_FORCE_ALLOW:
1186                     // Record that we are definitely going to allow it (unless another plugin
1187                     // returns _PREVENT).
1188                     $forceallow = true;
1189                     break;
1190                 case core_user::VIEWPROFILE_PREVENT:
1191                     // If any plugin returns PREVENT then we return false, regardless of what
1192                     // other plugins said.
1193                     return false;
1194             }
1195         }
1196     }
1197     if ($forceallow) {
1198         return true;
1199     }
1201     // Course contacts have visible profiles always.
1202     if (has_coursecontact_role($user->id)) {
1203         return true;
1204     }
1206     // If we're only checking the capabilities in the single provided course.
1207     if (isset($course)) {
1208         // Confirm that $user is enrolled in the $course we're checking.
1209         if (is_enrolled(context_course::instance($course->id), $user)) {
1210             $userscourses = array($course);
1211         }
1212     } else {
1213         // Else we're checking whether the current user can view $user's profile anywhere, so check user context first.
1214         if (empty($usercontext)) {
1215             $usercontext = context_user::instance($user->id);
1216         }
1217         if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) {
1218             return true;
1219         }
1220         // This returns context information, so we can preload below.
1221         $userscourses = enrol_get_all_users_courses($user->id);
1222     }
1224     if (empty($userscourses)) {
1225         return false;
1226     }
1228     foreach ($userscourses as $userscourse) {
1229         context_helper::preload_from_record($userscourse);
1230         $coursecontext = context_course::instance($userscourse->id);
1231         if (has_capability('moodle/user:viewdetails', $coursecontext) ||
1232             has_capability('moodle/user:viewalldetails', $coursecontext)) {
1233             if (!groups_user_groups_visible($userscourse, $user->id)) {
1234                 // Not a member of the same group.
1235                 continue;
1236             }
1237             return true;
1238         }
1239     }
1240     return false;
1243 /**
1244  * Returns users tagged with a specified tag.
1245  *
1246  * @param core_tag_tag $tag
1247  * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1248  *             are displayed on the page and the per-page limit may be bigger
1249  * @param int $fromctx context id where the link was displayed, may be used by callbacks
1250  *            to display items in the same context first
1251  * @param int $ctx context id where to search for records
1252  * @param bool $rec search in subcontexts as well
1253  * @param int $page 0-based number of page being displayed
1254  * @return \core_tag\output\tagindex
1255  */
1256 function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1257     global $PAGE;
1259     if ($ctx && $ctx != context_system::instance()->id) {
1260         $usercount = 0;
1261     } else {
1262         // Users can only be displayed in system context.
1263         $usercount = $tag->count_tagged_items('core', 'user',
1264                 'it.deleted=:notdeleted', array('notdeleted' => 0));
1265     }
1266     $perpage = $exclusivemode ? 24 : 5;
1267     $content = '';
1268     $totalpages = ceil($usercount / $perpage);
1270     if ($usercount) {
1271         $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage,
1272                 'it.deleted=:notdeleted', array('notdeleted' => 0));
1273         $renderer = $PAGE->get_renderer('core', 'user');
1274         $content .= $renderer->user_list($userlist, $exclusivemode);
1275     }
1277     return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1278             $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1281 /**
1282  * Returns the SQL used by the participants table.
1283  *
1284  * @param int $courseid The course id
1285  * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
1286  * @param int $accesssince The time since last access, 0 means any time
1287  * @param int $roleid The role id, 0 means all roles
1288  * @param int $enrolid The enrolment id, 0 means all enrolment methods will be returned.
1289  * @param int $statusid The user enrolment status, -1 means all enrolments regardless of the status will be returned, if allowed.
1290  * @param string|array $search The search that was performed, empty means perform no search
1291  * @param string $additionalwhere Any additional SQL to add to where
1292  * @param array $additionalparams The additional params
1293  * @return array
1294  */
1295 function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
1296                                    $search = '', $additionalwhere = '', $additionalparams = array()) {
1297     global $DB, $USER, $CFG;
1299     // Get the context.
1300     $context = \context_course::instance($courseid, MUST_EXIST);
1302     $isfrontpage = ($courseid == SITEID);
1304     // Default filter settings. We only show active by default, especially if the user has no capability to review enrolments.
1305     $onlyactive = true;
1306     $onlysuspended = false;
1307     if (has_capability('moodle/course:enrolreview', $context)) {
1308         switch ($statusid) {
1309             case ENROL_USER_ACTIVE:
1310                 // Nothing to do here.
1311                 break;
1312             case ENROL_USER_SUSPENDED:
1313                 $onlyactive = false;
1314                 $onlysuspended = true;
1315                 break;
1316             default:
1317                 // If the user has capability to review user enrolments, but statusid is set to -1, set $onlyactive to false.
1318                 $onlyactive = false;
1319                 break;
1320         }
1321     }
1323     list($esql, $params) = get_enrolled_sql($context, null, $groupid, $onlyactive, $onlysuspended, $enrolid);
1325     $joins = array('FROM {user} u');
1326     $wheres = array();
1328     $userfields = get_extra_user_fields($context);
1329     $userfieldssql = user_picture::fields('u', $userfields);
1331     if ($isfrontpage) {
1332         $select = "SELECT $userfieldssql, u.lastaccess";
1333         $joins[] = "JOIN ($esql) e ON e.id = u.id"; // Everybody on the frontpage usually.
1334         if ($accesssince) {
1335             $wheres[] = user_get_user_lastaccess_sql($accesssince);
1336         }
1337     } else {
1338         $select = "SELECT $userfieldssql, COALESCE(ul.timeaccess, 0) AS lastaccess";
1339         $joins[] = "JOIN ($esql) e ON e.id = u.id"; // Course enrolled users only.
1340         // Not everybody has accessed the course yet.
1341         $joins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid)';
1342         $params['courseid'] = $courseid;
1343         if ($accesssince) {
1344             $wheres[] = user_get_course_lastaccess_sql($accesssince);
1345         }
1346     }
1348     // Performance hacks - we preload user contexts together with accounts.
1349     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1350     $ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)';
1351     $params['contextlevel'] = CONTEXT_USER;
1352     $select .= $ccselect;
1353     $joins[] = $ccjoin;
1355     // Limit list to users with some role only.
1356     if ($roleid) {
1357         // We want to query both the current context and parent contexts.
1358         list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true),
1359             SQL_PARAMS_NAMED, 'relatedctx');
1361         $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
1362         $params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
1363     }
1365     if (!empty($search)) {
1366         if (!is_array($search)) {
1367             $search = [$search];
1368         }
1369         foreach ($search as $index => $keyword) {
1370             $searchkey1 = 'search' . $index . '1';
1371             $searchkey2 = 'search' . $index . '2';
1372             $searchkey3 = 'search' . $index . '3';
1373             $searchkey4 = 'search' . $index . '4';
1374             $searchkey5 = 'search' . $index . '5';
1375             $searchkey6 = 'search' . $index . '6';
1376             $searchkey7 = 'search' . $index . '7';
1378             $conditions = array();
1379             // Search by fullname.
1380             $fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
1381             $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
1383             // Search by email.
1384             $email = $DB->sql_like('email', ':' . $searchkey2, false, false);
1385             if (!in_array('email', $userfields)) {
1386                 $maildisplay = 'maildisplay' . $index;
1387                 $userid1 = 'userid' . $index . '1';
1388                 // Prevent users who hide their email address from being found by others
1389                 // who aren't allowed to see hidden email addresses.
1390                 $email = "(". $email ." AND (" .
1391                         "u.maildisplay <> :$maildisplay " .
1392                         "OR u.id = :$userid1". // User can always find himself.
1393                         "))";
1394                 $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
1395                 $params[$userid1] = $USER->id;
1396             }
1397             $conditions[] = $email;
1399             // Search by idnumber.
1400             $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
1401             if (!in_array('idnumber', $userfields)) {
1402                 $userid2 = 'userid' . $index . '2';
1403                 // Users who aren't allowed to see idnumbers should at most find themselves
1404                 // when searching for an idnumber.
1405                 $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
1406                 $params[$userid2] = $USER->id;
1407             }
1408             $conditions[] = $idnumber;
1410             if (!empty($CFG->showuseridentity)) {
1411                 // Search all user identify fields.
1412                 $extrasearchfields = explode(',', $CFG->showuseridentity);
1413                 foreach ($extrasearchfields as $extrasearchfield) {
1414                     if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
1415                         // Already covered above. Search by country not supported.
1416                         continue;
1417                     }
1418                     $param = $searchkey3 . $extrasearchfield;
1419                     $condition = $DB->sql_like($extrasearchfield, ':' . $param, false, false);
1420                     $params[$param] = "%$keyword%";
1421                     if (!in_array($extrasearchfield, $userfields)) {
1422                         // User cannot see this field, but allow match if their own account.
1423                         $userid3 = 'userid' . $index . '3' . $extrasearchfield;
1424                         $condition = "(". $condition . " AND u.id = :$userid3)";
1425                         $params[$userid3] = $USER->id;
1426                     }
1427                     $conditions[] = $condition;
1428                 }
1429             }
1431             // Search by middlename.
1432             $middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false);
1433             $conditions[] = $middlename;
1435             // Search by alternatename.
1436             $alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false);
1437             $conditions[] = $alternatename;
1439             // Search by firstnamephonetic.
1440             $firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false);
1441             $conditions[] = $firstnamephonetic;
1443             // Search by lastnamephonetic.
1444             $lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false);
1445             $conditions[] = $lastnamephonetic;
1447             $wheres[] = "(". implode(" OR ", $conditions) .") ";
1448             $params[$searchkey1] = "%$keyword%";
1449             $params[$searchkey2] = "%$keyword%";
1450             $params[$searchkey3] = "%$keyword%";
1451             $params[$searchkey4] = "%$keyword%";
1452             $params[$searchkey5] = "%$keyword%";
1453             $params[$searchkey6] = "%$keyword%";
1454             $params[$searchkey7] = "%$keyword%";
1455         }
1456     }
1458     if (!empty($additionalwhere)) {
1459         $wheres[] = $additionalwhere;
1460         $params = array_merge($params, $additionalparams);
1461     }
1463     $from = implode("\n", $joins);
1464     if ($wheres) {
1465         $where = 'WHERE ' . implode(' AND ', $wheres);
1466     } else {
1467         $where = '';
1468     }
1470     return array($select, $from, $where, $params);
1473 /**
1474  * Returns the total number of participants for a given course.
1475  *
1476  * @param int $courseid The course id
1477  * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
1478  * @param int $accesssince The time since last access, 0 means any time
1479  * @param int $roleid The role id, 0 means all roles
1480  * @param int $enrolid The applied filter for the user enrolment ID.
1481  * @param int $status The applied filter for the user's enrolment status.
1482  * @param string|array $search The search that was performed, empty means perform no search
1483  * @param string $additionalwhere Any additional SQL to add to where
1484  * @param array $additionalparams The additional params
1485  * @return int
1486  */
1487 function user_get_total_participants($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
1488                                      $search = '', $additionalwhere = '', $additionalparams = array()) {
1489     global $DB;
1491     list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
1492         $statusid, $search, $additionalwhere, $additionalparams);
1494     return $DB->count_records_sql("SELECT COUNT(u.id) $from $where", $params);
1497 /**
1498  * Returns the participants for a given course.
1499  *
1500  * @param int $courseid The course id
1501  * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
1502  * @param int $accesssince The time since last access
1503  * @param int $roleid The role id
1504  * @param int $enrolid The applied filter for the user enrolment ID.
1505  * @param int $status The applied filter for the user's enrolment status.
1506  * @param string $search The search that was performed
1507  * @param string $additionalwhere Any additional SQL to add to where
1508  * @param array $additionalparams The additional params
1509  * @param string $sort The SQL sort
1510  * @param int $limitfrom return a subset of records, starting at this point (optional).
1511  * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1512  * @return moodle_recordset
1513  */
1514 function user_get_participants($courseid, $groupid = 0, $accesssince, $roleid, $enrolid = 0, $statusid, $search,
1515                                $additionalwhere = '', $additionalparams = array(), $sort = '', $limitfrom = 0, $limitnum = 0) {
1516     global $DB;
1518     list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
1519         $statusid, $search, $additionalwhere, $additionalparams);
1521     return $DB->get_recordset_sql("$select $from $where $sort", $params, $limitfrom, $limitnum);
1524 /**
1525  * Returns SQL that can be used to limit a query to a period where the user last accessed a course.
1526  *
1527  * @param int $accesssince The time since last access
1528  * @param string $tableprefix
1529  * @return string
1530  */
1531 function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul') {
1532     if (empty($accesssince)) {
1533         return '';
1534     }
1536     if ($accesssince == -1) { // Never.
1537         return $tableprefix . '.timeaccess = 0';
1538     } else {
1539         return $tableprefix . '.timeaccess != 0 AND ' . $tableprefix . '.timeaccess < ' . $accesssince;
1540     }
1543 /**
1544  * Returns SQL that can be used to limit a query to a period where the user last accessed the system.
1545  *
1546  * @param int $accesssince The time since last access
1547  * @param string $tableprefix
1548  * @return string
1549  */
1550 function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u') {
1551     if (empty($accesssince)) {
1552         return '';
1553     }
1555     if ($accesssince == -1) { // Never.
1556         return $tableprefix . '.lastaccess = 0';
1557     } else {
1558         return $tableprefix . '.lastaccess != 0 AND ' . $tableprefix . '.lastaccess < ' . $accesssince;
1559     }
1562 /**
1563  * Callback for inplace editable API.
1564  *
1565  * @param string $itemtype - Only user_roles is supported.
1566  * @param string $itemid - Courseid and userid separated by a :
1567  * @param string $newvalue - json encoded list of roleids.
1568  * @return \core\output\inplace_editable
1569  */
1570 function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
1571     if ($itemtype === 'user_roles') {
1572         return \core_user\output\user_roles_editable::update($itemid, $newvalue);
1573     }
1576 /**
1577  * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
1578  *
1579  * @param integer $userid
1580  * @param string $fieldname
1581  * @return string $purpose (empty string if there is no mapping).
1582  */
1583 function user_edit_map_field_purpose($userid, $fieldname) {
1584     global $USER;
1586     $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
1587     // These are the fields considered valid to map and auto fill from a browser.
1588     // We do not include fields that are in a collapsed section by default because
1589     // the browser could auto-fill the field and cause a new value to be saved when
1590     // that field was never visible.
1591     $validmappings = array(
1592         'username' => 'username',
1593         'password' => 'current-password',
1594         'firstname' => 'given-name',
1595         'lastname' => 'family-name',
1596         'middlename' => 'additional-name',
1597         'email' => 'email',
1598         'country' => 'country',
1599         'lang' => 'language'
1600     );
1602     $purpose = '';
1603     if (!$currentuser) {
1604         // Do not set a purpose.
1605         $purpose = '';
1606     }
1607     if (isset($validmappings[$fieldname])) {
1608         $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
1609     }
1611     return $purpose;