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