MDL-66178 user: filter course participants for users with no roles.
[moodle.git] / user / lib.php
CommitLineData
fb79269b 1<?php
fb79269b 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 *
a2ed6e69
SH
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
fb79269b 23 */
24
bb869f05
DW
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
fb79269b 32/**
33 * Creates a user
adfb459c 34 *
a2ed6e69 35 * @throws moodle_exception
bb78e249
RT
36 * @param stdClass $user user to create
37 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
38 * @param bool $triggerevent set false if user_created event should not be triggred.
39 * This will not affect user_password_updated event triggering.
fb79269b 40 * @return int id of the newly created user
41 */
2b55cb1b 42function user_create_user($user, $updatepassword = true, $triggerevent = true) {
ac9768fc 43 global $DB;
fb79269b 44
bb78e249 45 // Set the timecreate field to the current time.
fb79269b 46 if (!is_object($user)) {
8bf0f207 47 $user = (object) $user;
fb79269b 48 }
bd0f26bd 49
bb78e249 50 // Check username.
784883e1
DM
51 if (trim($user->username) === '') {
52 throw new moodle_exception('invalidusernameblank');
53 }
54
2f1e464a 55 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c 56 throw new moodle_exception('usernamelowercase');
784883e1
DM
57 }
58
59 if ($user->username !== core_user::clean_field($user->username, 'username')) {
60 throw new moodle_exception('invalidusername');
45b4464c
JM
61 }
62
bb78e249
RT
63 // Save the password in a temp value for later.
64 if ($updatepassword && isset($user->password)) {
adfb459c 65
bb78e249 66 // Check password toward the password policy.
ad9c96e5 67 if (!check_password_policy($user->password, $errmsg, $user)) {
adfb459c
JM
68 throw new moodle_exception($errmsg);
69 }
70
71 $userpassword = $user->password;
72 unset($user->password);
73 }
bd0f26bd 74
9f7379e9 75 // Apply default values for user preferences that are stored in users table.
7a067206 76 if (!isset($user->calendartype)) {
ac9768fc 77 $user->calendartype = core_user::get_property_default('calendartype');
8bf0f207 78 }
9f7379e9 79 if (!isset($user->maildisplay)) {
ac9768fc 80 $user->maildisplay = core_user::get_property_default('maildisplay');
9f7379e9
MG
81 }
82 if (!isset($user->mailformat)) {
ac9768fc 83 $user->mailformat = core_user::get_property_default('mailformat');
9f7379e9
MG
84 }
85 if (!isset($user->maildigest)) {
ac9768fc 86 $user->maildigest = core_user::get_property_default('maildigest');
9f7379e9
MG
87 }
88 if (!isset($user->autosubscribe)) {
ac9768fc 89 $user->autosubscribe = core_user::get_property_default('autosubscribe');
9f7379e9
MG
90 }
91 if (!isset($user->trackforums)) {
ac9768fc 92 $user->trackforums = core_user::get_property_default('trackforums');
9f7379e9 93 }
7e670032 94 if (!isset($user->lang)) {
ac9768fc 95 $user->lang = core_user::get_property_default('lang');
7e670032 96 }
9f7379e9 97
fb79269b 98 $user->timecreated = time();
bd0f26bd 99 $user->timemodified = $user->timecreated;
fb79269b 100
7a067206
SL
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
bb78e249 110 // Insert the user into the database.
fb79269b 111 $newuserid = $DB->insert_record('user', $user);
112
bb78e249
RT
113 // Create USER context for this user.
114 $usercontext = context_user::instance($newuserid);
b6dcb7d9 115
bb78e249 116 // Update user password if necessary.
adfb459c 117 if (isset($userpassword)) {
bb78e249
RT
118 // Get full database user row, in case auth is default.
119 $newuser = $DB->get_record('user', array('id' => $newuserid));
adfb459c
JM
120 $authplugin = get_auth_plugin($newuser->auth);
121 $authplugin->user_update_password($newuser, $userpassword);
122 }
123
2b55cb1b
RT
124 // Trigger event If required.
125 if ($triggerevent) {
126 \core\event\user_created::create_from_userid($newuserid)->trigger();
127 }
adfb459c 128
c654e350
SA
129 // Purge the associated caches for the current user only.
130 $presignupcache = \cache::make('core', 'presignup');
131 $presignupcache->purge_current_user();
204178d6 132
fb79269b 133 return $newuserid;
fb79269b 134}
135
136/**
137 * Update a user with a user object (will compare against the ID)
adfb459c 138 *
a2ed6e69 139 * @throws moodle_exception
bb78e249
RT
140 * @param stdClass $user the user to update
141 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
142 * @param bool $triggerevent set false if user_updated event should not be triggred.
143 * This will not affect user_password_updated event triggering.
fb79269b 144 */
2b55cb1b 145function user_update_user($user, $updatepassword = true, $triggerevent = true) {
fb79269b 146 global $DB;
bd0f26bd 147
a2ed6e69 148 // Set the timecreate field to the current time.
bd0f26bd 149 if (!is_object($user)) {
8bf0f207 150 $user = (object) $user;
bd0f26bd 151 }
adfb459c 152
a2ed6e69 153 // Check username.
45b4464c 154 if (isset($user->username)) {
2f1e464a 155 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c
JM
156 throw new moodle_exception('usernamelowercase');
157 } else {
7a067206 158 if ($user->username !== core_user::clean_field($user->username, 'username')) {
45b4464c
JM
159 throw new moodle_exception('invalidusername');
160 }
161 }
162 }
163
bb78e249
RT
164 // Unset password here, for updating later, if password update is required.
165 if ($updatepassword && isset($user->password)) {
adfb459c 166
a2ed6e69 167 // Check password toward the password policy.
ad9c96e5 168 if (!check_password_policy($user->password, $errmsg, $user)) {
adfb459c
JM
169 throw new moodle_exception($errmsg);
170 }
171
9e63c0ff
FS
172 $passwd = $user->password;
173 unset($user->password);
174 }
bd0f26bd 175
8bf0f207 176 // Make sure calendartype, if set, is valid.
7a067206 177 if (empty($user->calendartype)) {
8bf0f207
MN
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
bd0f26bd 182 $user->timemodified = time();
7a067206
SL
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
fb79269b 193 $DB->update_record('user', $user);
b6dcb7d9 194
bb78e249
RT
195 if ($updatepassword) {
196 // Get full user record.
197 $updateduser = $DB->get_record('user', array('id' => $user->id));
9e63c0ff 198
a2ed6e69 199 // If password was set, then update its hash.
bb78e249
RT
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 }
adfb459c
JM
205 }
206 }
2b55cb1b
RT
207 // Trigger event if required.
208 if ($triggerevent) {
209 \core\event\user_updated::create_from_userid($user->id)->trigger();
210 }
adfb459c 211}
fb79269b 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) {
45fb2cf8 222 return delete_user($user);
fb79269b 223}
224
225/**
226 * Get users by id
fb79269b 227 *
a2ed6e69
SH
228 * @param array $userids id of users to retrieve
229 * @return array
fb79269b 230 */
231function user_get_users_by_id($userids) {
232 global $DB;
233 return $DB->get_records_list('user', 'id', $userids);
234}
b1627a92 235
61c8e0d7
FM
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',
1f87a7f6 249 'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended', 'lastcourseaccess'
61c8e0d7
FM
250 );
251}
01479290
DC
252
253/**
254 *
a2ed6e69 255 * Give user record from mdl_user, build an array contains all user details.
93ce0e82
JM
256 *
257 * Warning: description file urls are 'webservice/pluginfile.php' is use.
258 * it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
259 *
a2ed6e69 260 * @throws moodle_exception
01479290 261 * @param stdClass $user user record from mdl_user
01479290 262 * @param stdClass $course moodle course
ad7612f5 263 * @param array $userfields required fields
d6731600 264 * @return array|null
01479290 265 */
ad7612f5 266function user_get_user_details($user, $course = null, array $userfields = array()) {
1f7273af 267 global $USER, $DB, $CFG, $PAGE;
a2ed6e69
SH
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.
01479290 270
61c8e0d7 271 $defaultfields = user_get_default_fields();
ad7612f5
DC
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
a2ed6e69 283 // Make sure id and fullname are included.
ad7612f5
DC
284 if (!in_array('id', $userfields)) {
285 $userfields[] = 'id';
286 }
287
288 if (!in_array('fullname', $userfields)) {
289 $userfields[] = 'fullname';
290 }
291
01479290 292 if (!empty($course)) {
43731030
FM
293 $context = context_course::instance($course->id);
294 $usercontext = context_user::instance($user->id);
1e539f64 295 $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
01479290 296 } else {
43731030 297 $context = context_user::instance($user->id);
01479290 298 $usercontext = $context;
1e539f64 299 $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
01479290
DC
300 }
301
302 $currentuser = ($user->id == $USER->id);
303 $isadmin = is_siteadmin($USER);
304
48a7b182
JM
305 $showuseridentityfields = get_extra_user_fields($context);
306
01479290
DC
307 if (!empty($course)) {
308 $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
309 } else {
310 $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
311 }
86477112 312 $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
01479290
DC
313 if (!empty($course)) {
314 $canviewuseremail = has_capability('moodle/course:useremail', $context);
315 } else {
316 $canviewuseremail = false;
317 }
a2ed6e69 318 $cannotviewdescription = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
01479290
DC
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)) {
a2ed6e69 326 // Skip this user details.
01479290
DC
327 return null;
328 }
329
330 $userdetails = array();
331 $userdetails['id'] = $user->id;
332
07d37084
DM
333 if (in_array('username', $userfields)) {
334 if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
335 $userdetails['username'] = $user->username;
336 }
01479290
DC
337 }
338 if ($isadmin or $canviewfullnames) {
ad7612f5
DC
339 if (in_array('firstname', $userfields)) {
340 $userdetails['firstname'] = $user->firstname;
341 }
342 if (in_array('lastname', $userfields)) {
343 $userdetails['lastname'] = $user->lastname;
344 }
01479290 345 }
c157e137 346 $userdetails['fullname'] = fullname($user, $canviewfullnames);
01479290 347
ad7612f5 348 if (in_array('customfields', $userfields)) {
860f59b1 349 $categories = profile_get_user_fields_with_data_by_category($user->id);
ad7612f5 350 $userdetails['customfields'] = array();
860f59b1 351 foreach ($categories as $categoryid => $fields) {
b47fda71 352 foreach ($fields as $formfield) {
860f59b1
MC
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.
b47fda71 358 if ($formfield->field->datatype == 'text' or $formfield->field->datatype == 'textarea') {
860f59b1
MC
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,
b47fda71 367 'type' => $formfield->field->datatype, 'shortname' => $formfield->field->shortname);
f804c730 368 }
ad7612f5
DC
369 }
370 }
a2ed6e69 371 // Unset customfields if it's empty.
ad7612f5
DC
372 if (empty($userdetails['customfields'])) {
373 unset($userdetails['customfields']);
01479290 374 }
01479290
DC
375 }
376
a2ed6e69 377 // Profile image.
ad7612f5 378 if (in_array('profileimageurl', $userfields)) {
1f7273af
DP
379 $userpicture = new user_picture($user);
380 $userpicture->size = 1; // Size f1.
381 $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
ad7612f5
DC
382 }
383 if (in_array('profileimageurlsmall', $userfields)) {
1f7273af
DP
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);
ad7612f5 389 }
01479290 390
a2ed6e69 391 // Hidden user field.
01479290
DC
392 if ($canviewhiddenuserfields) {
393 $hiddenfields = array();
a2ed6e69
SH
394 // Address, phone1 and phone2 not appears in hidden fields list but require viewhiddenfields capability
395 // according to user/profile.php.
cb805753 396 if (!empty($user->address) && in_array('address', $userfields)) {
01479290
DC
397 $userdetails['address'] = $user->address;
398 }
01479290
DC
399 } else {
400 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
401 }
402
cb805753 403 if (!empty($user->phone1) && in_array('phone1', $userfields) &&
58f739c5 404 (in_array('phone1', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
405 $userdetails['phone1'] = $user->phone1;
406 }
cb805753 407 if (!empty($user->phone2) && in_array('phone2', $userfields) &&
58f739c5 408 (in_array('phone2', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
409 $userdetails['phone2'] = $user->phone2;
410 }
411
acf64596
JM
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);
01479290
DC
419 }
420 }
421
ad7612f5 422 if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
01479290
DC
423 $userdetails['country'] = $user->country;
424 }
425
ad7612f5 426 if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
01479290
DC
427 $userdetails['city'] = $user->city;
428 }
429
ad7612f5 430 if (in_array('url', $userfields) && $user->url && (!isset($hiddenfields['webpage']) or $isadmin)) {
01479290
DC
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
ad7612f5 439 if (in_array('icq', $userfields) && $user->icq && (!isset($hiddenfields['icqnumber']) or $isadmin)) {
01479290
DC
440 $userdetails['icq'] = $user->icq;
441 }
442
ad7612f5 443 if (in_array('skype', $userfields) && $user->skype && (!isset($hiddenfields['skypeid']) or $isadmin)) {
01479290
DC
444 $userdetails['skype'] = $user->skype;
445 }
ad7612f5 446 if (in_array('yahoo', $userfields) && $user->yahoo && (!isset($hiddenfields['yahooid']) or $isadmin)) {
01479290
DC
447 $userdetails['yahoo'] = $user->yahoo;
448 }
ad7612f5 449 if (in_array('aim', $userfields) && $user->aim && (!isset($hiddenfields['aimid']) or $isadmin)) {
01479290
DC
450 $userdetails['aim'] = $user->aim;
451 }
ad7612f5 452 if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
01479290
DC
453 $userdetails['msn'] = $user->msn;
454 }
511db621 455 if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
84900149 456 $userdetails['suspended'] = (bool)$user->suspended;
511db621 457 }
01479290 458
ad7612f5 459 if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
01479290
DC
460 if ($user->firstaccess) {
461 $userdetails['firstaccess'] = $user->firstaccess;
462 } else {
463 $userdetails['firstaccess'] = 0;
464 }
465 }
ad7612f5 466 if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
01479290
DC
467 if ($user->lastaccess) {
468 $userdetails['lastaccess'] = $user->lastaccess;
469 } else {
470 $userdetails['lastaccess'] = 0;
471 }
472 }
473
1f87a7f6
JL
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
565f3f25
DB
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 )) {
90e30b96 492 $userdetails['email'] = $user->email;
01479290
DC
493 }
494
c4e868d5 495 if (in_array('interests', $userfields)) {
e11d7380 496 $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
c4e868d5
MG
497 if ($interests) {
498 $userdetails['interests'] = join(', ', $interests);
01479290
DC
499 }
500 }
501
a2ed6e69 502 // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
07d37084
DM
503 if (in_array('idnumber', $userfields) && $user->idnumber) {
504 if (in_array('idnumber', $showuseridentityfields) or $currentuser or
505 has_capability('moodle/user:viewalldetails', $context)) {
3a3f3b22
CW
506 $userdetails['idnumber'] = $user->idnumber;
507 }
48a7b182 508 }
07d37084
DM
509 if (in_array('institution', $userfields) && $user->institution) {
510 if (in_array('institution', $showuseridentityfields) or $currentuser or
511 has_capability('moodle/user:viewalldetails', $context)) {
01479290
DC
512 $userdetails['institution'] = $user->institution;
513 }
48a7b182 514 }
07d37084
DM
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)) {
01479290
DC
519 $userdetails['department'] = $user->department;
520 }
521 }
522
ad7612f5 523 if (in_array('roles', $userfields) && !empty($course)) {
a2ed6e69 524 // Not a big secret.
01479290
DC
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
a2ed6e69 537 // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
ad7612f5 538 if (in_array('groups', $userfields) && !empty($course) && $canaccessallgroups) {
93ce0e82
JM
539 $usergroups = groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid,
540 'g.id, g.name,g.description,g.descriptionformat');
01479290
DC
541 $userdetails['groups'] = array();
542 foreach ($usergroups as $group) {
93ce0e82
JM
543 list($group->description, $group->descriptionformat) =
544 external_format_text($group->description, $group->descriptionformat,
545 $context->id, 'group', 'description', $group->id);
a2ed6e69
SH
546 $userdetails['groups'][] = array('id' => $group->id, 'name' => $group->name,
547 'description' => $group->description, 'descriptionformat' => $group->descriptionformat);
01479290
DC
548 }
549 }
a2ed6e69 550 // List of courses where the user is enrolled.
ad7612f5 551 if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
01479290
DC
552 $enrolledcourses = array();
553 if ($mycourses = enrol_get_users_courses($user->id, true)) {
554 foreach ($mycourses as $mycourse) {
555 if ($mycourse->category) {
43731030 556 $coursecontext = context_course::instance($mycourse->id);
01479290
DC
557 $enrolledcourse = array();
558 $enrolledcourse['id'] = $mycourse->id;
43ff71a0 559 $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
8ebbb06a 560 $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
01479290
DC
561 $enrolledcourses[] = $enrolledcourse;
562 }
563 }
564 $userdetails['enrolledcourses'] = $enrolledcourses;
565 }
566 }
567
a2ed6e69 568 // User preferences.
ad7612f5 569 if (in_array('preferences', $userfields) && $currentuser) {
01479290
DC
570 $preferences = array();
571 $userpreferences = get_user_preferences();
a2ed6e69 572 foreach ($userpreferences as $prefname => $prefvalue) {
01479290 573 $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
a2ed6e69
SH
574 }
575 $userdetails['preferences'] = $preferences;
01479290 576 }
ad7612f5 577
b80caca1
DTR
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
6db24235
JL
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
01479290
DC
595 return $userdetails;
596}
597
86477112
FS
598/**
599 * Tries to obtain user details, either recurring directly to the user's system profile
c70b9853 600 * or through one of the user's course enrollments (course profile).
86477112 601 *
a2ed6e69 602 * @param stdClass $user The user.
c70b9853 603 * @return array if unsuccessful or the allowed user details.
86477112
FS
604 */
605function user_get_user_details_courses($user) {
606 global $USER;
607 $userdetails = null;
608
c70b9853
JM
609 $systemprofile = false;
610 if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
611 $systemprofile = true;
612 }
86477112 613
c70b9853 614 // Try using system profile.
86477112
FS
615 if ($systemprofile) {
616 $userdetails = user_get_user_details($user, null);
617 } else {
c70b9853 618 // Try through course profile.
152e41cc
JD
619 // Get the courses that the user is enrolled in (only active).
620 $courses = enrol_get_users_courses($user->id, true);
86477112 621 foreach ($courses as $course) {
e4aaecd3 622 if (user_can_view_profile($user, $course)) {
86477112
FS
623 $userdetails = user_get_user_details($user, $course);
624 }
625 }
626 }
627
628 return $userdetails;
629}
630
631/**
c70b9853
JM
632 * Check if $USER have the necessary capabilities to obtain user details.
633 *
a2ed6e69
SH
634 * @param stdClass $user
635 * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
c70b9853 636 * @return bool true if $USER can view user details.
86477112
FS
637 */
638function can_view_user_details_cap($user, $course = null) {
c70b9853 639 // Check $USER has the capability to view the user details at user context.
bea86e84 640 $usercontext = context_user::instance($user->id);
c70b9853
JM
641 $result = has_capability('moodle/user:viewdetails', $usercontext);
642 // Otherwise can $USER see them at course context.
643 if (!$result && !empty($course)) {
bea86e84 644 $context = context_course::instance($course->id);
c70b9853 645 $result = has_capability('moodle/user:viewdetails', $context);
86477112
FS
646 }
647 return $result;
648}
649
b1627a92
DC
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
a2ed6e69 655 * @return array
b1627a92 656 */
b38e2e28 657function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
a2ed6e69 658 return array('user-profile' => get_string('page-user-profile', 'pagetype'));
d6731600 659}
52dc1de7
AA
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
b865a3e2 686/**
22893d6f
JC
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.
b865a3e2
JC
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;
088fcf90 702 $children = array();
b865a3e2
JC
703 foreach ($lines as $line) {
704 $line = trim($line);
705 $bits = explode('|', $line, 3);
22893d6f
JC
706 $itemtype = 'link';
707 if (preg_match("/^#+$/", $line)) {
708 $itemtype = 'divider';
709 } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
b865a3e2
JC
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();
22893d6f
JC
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 }
b865a3e2
JC
725
726 // Name processing.
727 $namebits = explode(',', $bits[0], 2);
728 if (count($namebits) == 2) {
06c27531
AN
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]);
d7dbef73 733 $child->titleidentifier = implode(',', $namebits);
06c27531
AN
734 }
735 }
736 if (empty($child->title)) {
b865a3e2
JC
737 // Use it as is, don't even clean it.
738 $child->title = $bits[0];
d7dbef73 739 $child->titleidentifier = str_replace(" ", "-", $bits[0]);
b865a3e2
JC
740 }
741
742 // URL processing.
743 if (!array_key_exists(1, $bits) or empty($bits[1])) {
22893d6f 744 // Set the url to null, and set the itemtype to invalid.
b865a3e2 745 $bits[1] = null;
22893d6f 746 $child->itemtype = "invalid";
b865a3e2 747 } else {
4887d152 748 // Nasty hack to replace the grades with the direct url.
c9451960
AG
749 if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
750 $bits[1] = user_mygrades_url();
751 }
752
b865a3e2
JC
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.
df8eec52
KO
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 }
b865a3e2
JC
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
6da0e4cf
JC
792/**
793 * Get a list of essential user navigation items.
794 *
795 * @param stdclass $user user object.
796 * @param moodle_page $page page object.
9dcd5035
DB
797 * @param array $options associative array.
798 * options are:
799 * - avatarsize=35 (size of avatar image)
6da0e4cf
JC
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 */
c68b3206 842function user_get_user_navigation_info($user, $page, $options = array()) {
6da0e4cf
JC
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, true);
857 $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
858 'id' => $user->id
859 ));
9dcd5035
DB
860
861 $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
862 if (!empty($options['avatarsize'])) {
863 $avataroptions['size'] = $options['avatarsize'];
864 }
6da0e4cf 865 $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
9dcd5035 866 $user, $avataroptions
6da0e4cf
JC
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)) {
fa30dd8c
JD
881 // Don't reset the count either, as login_info() still needs it too.
882 if ($count = user_count_login_failures($user, false)) {
6da0e4cf
JC
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
81d7de1a 894 // Links: Dashboard.
6da0e4cf 895 $myhome = new stdClass();
22893d6f 896 $myhome->itemtype = 'link';
6da0e4cf
JC
897 $myhome->url = new moodle_url('/my/');
898 $myhome->title = get_string('mymoodle', 'admin');
d7dbef73 899 $myhome->titleidentifier = 'mymoodle,admin';
f3dacc38 900 $myhome->pix = "i/dashboard";
6da0e4cf
JC
901 $returnobject->navitems[] = $myhome;
902
903 // Links: My Profile.
904 $myprofile = new stdClass();
22893d6f 905 $myprofile->itemtype = 'link';
6da0e4cf 906 $myprofile->url = new moodle_url('/user/profile.php', array('id' => $user->id));
4887d152 907 $myprofile->title = get_string('profile');
d7dbef73 908 $myprofile->titleidentifier = 'profile,moodle';
6da0e4cf
JC
909 $myprofile->pix = "i/user";
910 $returnobject->navitems[] = $myprofile;
911
6da0e4cf 912 $returnobject->metadata['asotherrole'] = false;
6da0e4cf 913
70b03eff 914 // Before we add the last items (usually a logout + switch role link), add any
7095f5be
DW
915 // custom-defined items.
916 $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
917 foreach ($customitems as $item) {
918 $returnobject->navitems[] = $item;
6da0e4cf
JC
919 }
920
70b03eff 921
6da0e4cf
JC
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, true);
929 $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', array(
930 'id' => $realuser->id
931 ));
c68b3206 932 $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
6da0e4cf
JC
933
934 // Build a user-revert link.
935 $userrevert = new stdClass();
22893d6f 936 $userrevert->itemtype = 'link';
6da0e4cf
JC
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');
d7dbef73 943 $userrevert->titleidentifier = 'logout,moodle';
7095f5be 944 $returnobject->navitems[] = $userrevert;
6da0e4cf 945
7095f5be 946 } else {
6da0e4cf 947
6da0e4cf
JC
948 // Build a logout link.
949 $logout = new stdClass();
22893d6f 950 $logout->itemtype = 'link';
6da0e4cf
JC
951 $logout->url = new moodle_url('/login/logout.php', array('sesskey' => sesskey()));
952 $logout->pix = "a/logout";
953 $logout->title = get_string('logout');
d7dbef73 954 $logout->titleidentifier = 'logout,moodle';
7095f5be 955 $returnobject->navitems[] = $logout;
6da0e4cf
JC
956 }
957
70b03eff
DW
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);
b865a3e2 976
70b03eff
DW
977 }
978 } else {
7dba450f
DW
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 }
6da0e4cf
JC
994 }
995
996 return $returnobject;
1d658535
PS
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;
1d658535
PS
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;
1d658535
PS
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}
3221718e
JL
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}
0ff203b6
JL
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}
c9451960
AG
1120
1121/**
4887d152 1122 * Returns the url to use for the "Grades" link in the user navigation.
c9451960
AG
1123 *
1124 * @param int $userid The user's ID.
1125 * @param int $courseid The course ID if available.
4887d152 1126 * @return mixed A URL to be directed to for "Grades".
c9451960
AG
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}
66a43cd5
AG
1147
1148/**
eb01e70b
JD
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.
66a43cd5
AG
1155 *
1156 * @param object $user The other user's details.
eb01e70b 1157 * @param object $course if provided, only check permissions in this course.
66a43cd5
AG
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
eb01e70b 1168 // Do we need to be logged in?
58d85af2 1169 if (empty($CFG->forceloginforprofiles)) {
66a43cd5 1170 return true;
58d85af2
AA
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 }
66a43cd5
AG
1176 }
1177
eb01e70b 1178 // Current user can always view their profile.
58d85af2 1179 if ($USER->id == $user->id) {
66a43cd5
AG
1180 return true;
1181 }
1182
bef86c66 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
eb01e70b 1210 // Course contacts have visible profiles always.
66a43cd5
AG
1211 if (has_coursecontact_role($user->id)) {
1212 return true;
1213 }
1214
eb01e70b 1215 // If we're only checking the capabilities in the single provided course.
66a43cd5 1216 if (isset($course)) {
eb01e70b
JD
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 }
66a43cd5 1221 } else {
eb01e70b
JD
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 }
067accce
JD
1229 // This returns context information, so we can preload below.
1230 $userscourses = enrol_get_all_users_courses($user->id);
66a43cd5 1231 }
3e40dc8c 1232
067accce 1233 if (empty($userscourses)) {
3e40dc8c
AA
1234 return false;
1235 }
1236
067accce
JD
1237 foreach ($userscourses as $userscourse) {
1238 context_helper::preload_from_record($userscourse);
1239 $coursecontext = context_course::instance($userscourse->id);
eb01e70b
JD
1240 if (has_capability('moodle/user:viewdetails', $coursecontext) ||
1241 has_capability('moodle/user:viewalldetails', $coursecontext)) {
067accce 1242 if (!groups_user_groups_visible($userscourse, $user->id)) {
66a43cd5
AG
1243 // Not a member of the same group.
1244 continue;
1245 }
1246 return true;
1247 }
1248 }
1249 return false;
1250}
c4e868d5
MG
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}
bc47b706
MN
1289
1290/**
1291 * Returns the SQL used by the participants table.
1292 *
1293 * @param int $courseid The course id
5290d060 1294 * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
bc47b706 1295 * @param int $accesssince The time since last access, 0 means any time
90ce66a9 1296 * @param int $roleid The role id, 0 means all roles and -1 no roles
9651e491
JP
1297 * @param int $enrolid The enrolment id, 0 means all enrolment methods will be returned.
1298 * @param int $statusid The user enrolment status, -1 means all enrolments regardless of the status will be returned, if allowed.
1299 * @param string|array $search The search that was performed, empty means perform no search
bc47b706
MN
1300 * @param string $additionalwhere Any additional SQL to add to where
1301 * @param array $additionalparams The additional params
1302 * @return array
1303 */
9651e491
JP
1304function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
1305 $search = '', $additionalwhere = '', $additionalparams = array()) {
e52856bc 1306 global $DB, $USER, $CFG;
bc47b706
MN
1307
1308 // Get the context.
1309 $context = \context_course::instance($courseid, MUST_EXIST);
1310
1311 $isfrontpage = ($courseid == SITEID);
1312
9651e491
JP
1313 // Default filter settings. We only show active by default, especially if the user has no capability to review enrolments.
1314 $onlyactive = true;
1315 $onlysuspended = false;
1316 if (has_capability('moodle/course:enrolreview', $context)) {
1317 switch ($statusid) {
1318 case ENROL_USER_ACTIVE:
1319 // Nothing to do here.
1320 break;
1321 case ENROL_USER_SUSPENDED:
1322 $onlyactive = false;
1323 $onlysuspended = true;
1324 break;
1325 default:
1326 // If the user has capability to review user enrolments, but statusid is set to -1, set $onlyactive to false.
1327 $onlyactive = false;
1328 break;
1329 }
1330 }
1331
1332 list($esql, $params) = get_enrolled_sql($context, null, $groupid, $onlyactive, $onlysuspended, $enrolid);
bc47b706
MN
1333
1334 $joins = array('FROM {user} u');
1335 $wheres = array();
1336
aa949cb2 1337 $userfields = get_extra_user_fields($context);
a9991533 1338 $userfieldssql = user_picture::fields('u', $userfields);
bc47b706
MN
1339
1340 if ($isfrontpage) {
a9991533 1341 $select = "SELECT $userfieldssql, u.lastaccess";
bc47b706
MN
1342 $joins[] = "JOIN ($esql) e ON e.id = u.id"; // Everybody on the frontpage usually.
1343 if ($accesssince) {
1344 $wheres[] = user_get_user_lastaccess_sql($accesssince);
1345 }
1346 } else {
a9991533 1347 $select = "SELECT $userfieldssql, COALESCE(ul.timeaccess, 0) AS lastaccess";
bc47b706
MN
1348 $joins[] = "JOIN ($esql) e ON e.id = u.id"; // Course enrolled users only.
1349 // Not everybody has accessed the course yet.
1350 $joins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid)';
1351 $params['courseid'] = $courseid;
1352 if ($accesssince) {
1353 $wheres[] = user_get_course_lastaccess_sql($accesssince);
1354 }
1355 }
1356
1357 // Performance hacks - we preload user contexts together with accounts.
1358 $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
1359 $ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)';
1360 $params['contextlevel'] = CONTEXT_USER;
1361 $select .= $ccselect;
1362 $joins[] = $ccjoin;
1363
1364 // Limit list to users with some role only.
1365 if ($roleid) {
1366 // We want to query both the current context and parent contexts.
1367 list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true),
1368 SQL_PARAMS_NAMED, 'relatedctx');
1369
90ce66a9
PH
1370 // Get users without any role.
1371 if ($roleid == -1) {
1372 $wheres[] = "u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid $relatedctxsql)";
1373 $params = array_merge($params, $relatedctxparams);
1374 } else {
1375 $wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
1376 $params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
1377 }
bc47b706
MN
1378 }
1379
1380 if (!empty($search)) {
9651e491
JP
1381 if (!is_array($search)) {
1382 $search = [$search];
1383 }
1384 foreach ($search as $index => $keyword) {
1385 $searchkey1 = 'search' . $index . '1';
1386 $searchkey2 = 'search' . $index . '2';
1387 $searchkey3 = 'search' . $index . '3';
358b10a3
IS
1388 $searchkey4 = 'search' . $index . '4';
1389 $searchkey5 = 'search' . $index . '5';
1390 $searchkey6 = 'search' . $index . '6';
1391 $searchkey7 = 'search' . $index . '7';
4bcb0a9a
SA
1392
1393 $conditions = array();
1394 // Search by fullname.
9651e491 1395 $fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
4bcb0a9a
SA
1396 $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
1397
1398 // Search by email.
1399 $email = $DB->sql_like('email', ':' . $searchkey2, false, false);
1400 if (!in_array('email', $userfields)) {
1401 $maildisplay = 'maildisplay' . $index;
1402 $userid1 = 'userid' . $index . '1';
1403 // Prevent users who hide their email address from being found by others
1404 // who aren't allowed to see hidden email addresses.
1405 $email = "(". $email ." AND (" .
1406 "u.maildisplay <> :$maildisplay " .
1407 "OR u.id = :$userid1". // User can always find himself.
1408 "))";
1409 $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
1410 $params[$userid1] = $USER->id;
1411 }
1412 $conditions[] = $email;
1413
1414 // Search by idnumber.
1415 $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
1416 if (!in_array('idnumber', $userfields)) {
1417 $userid2 = 'userid' . $index . '2';
1418 // Users who aren't allowed to see idnumbers should at most find themselves
1419 // when searching for an idnumber.
1420 $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
1421 $params[$userid2] = $USER->id;
1422 }
1423 $conditions[] = $idnumber;
1424
e52856bc
JP
1425 if (!empty($CFG->showuseridentity)) {
1426 // Search all user identify fields.
1427 $extrasearchfields = explode(',', $CFG->showuseridentity);
1428 foreach ($extrasearchfields as $extrasearchfield) {
1429 if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
1430 // Already covered above. Search by country not supported.
1431 continue;
1432 }
1433 $param = $searchkey3 . $extrasearchfield;
1434 $condition = $DB->sql_like($extrasearchfield, ':' . $param, false, false);
1435 $params[$param] = "%$keyword%";
1436 if (!in_array($extrasearchfield, $userfields)) {
1437 // User cannot see this field, but allow match if their own account.
1438 $userid3 = 'userid' . $index . '3' . $extrasearchfield;
1439 $condition = "(". $condition . " AND u.id = :$userid3)";
1440 $params[$userid3] = $USER->id;
1441 }
1442 $conditions[] = $condition;
1443 }
1444 }
1445
358b10a3
IS
1446 // Search by middlename.
1447 $middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false);
1448 $conditions[] = $middlename;
1449
1450 // Search by alternatename.
1451 $alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false);
1452 $conditions[] = $alternatename;
1453
1454 // Search by firstnamephonetic.
1455 $firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false);
1456 $conditions[] = $firstnamephonetic;
1457
1458 // Search by lastnamephonetic.
1459 $lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false);
1460 $conditions[] = $lastnamephonetic;
1461
4bcb0a9a 1462 $wheres[] = "(". implode(" OR ", $conditions) .") ";
9651e491
JP
1463 $params[$searchkey1] = "%$keyword%";
1464 $params[$searchkey2] = "%$keyword%";
1465 $params[$searchkey3] = "%$keyword%";
358b10a3
IS
1466 $params[$searchkey4] = "%$keyword%";
1467 $params[$searchkey5] = "%$keyword%";
1468 $params[$searchkey6] = "%$keyword%";
1469 $params[$searchkey7] = "%$keyword%";
9651e491 1470 }
bc47b706
MN
1471 }
1472
1473 if (!empty($additionalwhere)) {
1474 $wheres[] = $additionalwhere;
1475 $params = array_merge($params, $additionalparams);
1476 }
1477
1478 $from = implode("\n", $joins);
1479 if ($wheres) {
1480 $where = 'WHERE ' . implode(' AND ', $wheres);
1481 } else {
1482 $where = '';
1483 }
1484
1485 return array($select, $from, $where, $params);
1486}
1487
1488/**
1489 * Returns the total number of participants for a given course.
1490 *
1491 * @param int $courseid The course id
5290d060 1492 * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
bc47b706
MN
1493 * @param int $accesssince The time since last access, 0 means any time
1494 * @param int $roleid The role id, 0 means all roles
9651e491
JP
1495 * @param int $enrolid The applied filter for the user enrolment ID.
1496 * @param int $status The applied filter for the user's enrolment status.
1497 * @param string|array $search The search that was performed, empty means perform no search
bc47b706
MN
1498 * @param string $additionalwhere Any additional SQL to add to where
1499 * @param array $additionalparams The additional params
1500 * @return int
1501 */
9651e491
JP
1502function user_get_total_participants($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
1503 $search = '', $additionalwhere = '', $additionalparams = array()) {
bc47b706
MN
1504 global $DB;
1505
9651e491
JP
1506 list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
1507 $statusid, $search, $additionalwhere, $additionalparams);
bc47b706
MN
1508
1509 return $DB->count_records_sql("SELECT COUNT(u.id) $from $where", $params);
1510}
1511
1512/**
1513 * Returns the participants for a given course.
1514 *
1515 * @param int $courseid The course id
5290d060 1516 * @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
bc47b706
MN
1517 * @param int $accesssince The time since last access
1518 * @param int $roleid The role id
9651e491
JP
1519 * @param int $enrolid The applied filter for the user enrolment ID.
1520 * @param int $status The applied filter for the user's enrolment status.
bc47b706
MN
1521 * @param string $search The search that was performed
1522 * @param string $additionalwhere Any additional SQL to add to where
1523 * @param array $additionalparams The additional params
1524 * @param string $sort The SQL sort
1525 * @param int $limitfrom return a subset of records, starting at this point (optional).
1526 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1527 * @return moodle_recordset
1528 */
9651e491
JP
1529function user_get_participants($courseid, $groupid = 0, $accesssince, $roleid, $enrolid = 0, $statusid, $search,
1530 $additionalwhere = '', $additionalparams = array(), $sort = '', $limitfrom = 0, $limitnum = 0) {
bc47b706
MN
1531 global $DB;
1532
9651e491
JP
1533 list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
1534 $statusid, $search, $additionalwhere, $additionalparams);
bc47b706
MN
1535
1536 return $DB->get_recordset_sql("$select $from $where $sort", $params, $limitfrom, $limitnum);
1537}
1538
1539/**
1540 * Returns SQL that can be used to limit a query to a period where the user last accessed a course.
1541 *
1542 * @param int $accesssince The time since last access
1543 * @param string $tableprefix
1544 * @return string
1545 */
1546function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul') {
1547 if (empty($accesssince)) {
1548 return '';
1549 }
1550
1551 if ($accesssince == -1) { // Never.
1552 return $tableprefix . '.timeaccess = 0';
1553 } else {
b93767f3 1554 return $tableprefix . '.timeaccess != 0 AND ' . $tableprefix . '.timeaccess < ' . $accesssince;
bc47b706
MN
1555 }
1556}
1557
1558/**
1559 * Returns SQL that can be used to limit a query to a period where the user last accessed the system.
1560 *
1561 * @param int $accesssince The time since last access
1562 * @param string $tableprefix
1563 * @return string
1564 */
1565function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u') {
1566 if (empty($accesssince)) {
1567 return '';
1568 }
1569
1570 if ($accesssince == -1) { // Never.
1571 return $tableprefix . '.lastaccess = 0';
1572 } else {
b93767f3 1573 return $tableprefix . '.lastaccess != 0 AND ' . $tableprefix . '.lastaccess < ' . $accesssince;
bc47b706
MN
1574 }
1575}
5d0b4765
DW
1576
1577/**
1578 * Callback for inplace editable API.
1579 *
1580 * @param string $itemtype - Only user_roles is supported.
73d0d562 1581 * @param string $itemid - Courseid and userid separated by a :
5d0b4765
DW
1582 * @param string $newvalue - json encoded list of roleids.
1583 * @return \core\output\inplace_editable
1584 */
1585function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
1586 if ($itemtype === 'user_roles') {
1587 return \core_user\output\user_roles_editable::update($itemid, $newvalue);
1588 }
1589}
2159983a
DW
1590
1591/**
1592 * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
1593 *
1594 * @param integer $userid
1595 * @param string $fieldname
1596 * @return string $purpose (empty string if there is no mapping).
1597 */
1598function user_edit_map_field_purpose($userid, $fieldname) {
1599 global $USER;
1600
1601 $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
1602 // These are the fields considered valid to map and auto fill from a browser.
1603 // We do not include fields that are in a collapsed section by default because
1604 // the browser could auto-fill the field and cause a new value to be saved when
1605 // that field was never visible.
1606 $validmappings = array(
1607 'username' => 'username',
1608 'password' => 'current-password',
1609 'firstname' => 'given-name',
1610 'lastname' => 'family-name',
1611 'middlename' => 'additional-name',
1612 'email' => 'email',
1613 'country' => 'country',
1614 'lang' => 'language'
1615 );
1616
1617 $purpose = '';
145a10f7
DW
1618 // Only set a purpose when editing your own user details.
1619 if ($currentuser && isset($validmappings[$fieldname])) {
2159983a
DW
1620 $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
1621 }
1622
1623 return $purpose;
1624}
1625