MDL-52781 core_user: add validation to user insertion and updating method
[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
25
26/**
27 * Creates a user
adfb459c 28 *
a2ed6e69 29 * @throws moodle_exception
bb78e249
RT
30 * @param stdClass $user user to create
31 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
32 * @param bool $triggerevent set false if user_created event should not be triggred.
33 * This will not affect user_password_updated event triggering.
fb79269b 34 * @return int id of the newly created user
35 */
2b55cb1b 36function user_create_user($user, $updatepassword = true, $triggerevent = true) {
8bf0f207 37 global $CFG, $DB;
fb79269b 38
bb78e249 39 // Set the timecreate field to the current time.
fb79269b 40 if (!is_object($user)) {
8bf0f207 41 $user = (object) $user;
fb79269b 42 }
bd0f26bd 43
bb78e249 44 // Check username.
2f1e464a 45 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c
JM
46 throw new moodle_exception('usernamelowercase');
47 } else {
7a067206 48 if ($user->username !== core_user::clean_field($user->username, 'username')) {
45b4464c
JM
49 throw new moodle_exception('invalidusername');
50 }
51 }
52
bb78e249
RT
53 // Save the password in a temp value for later.
54 if ($updatepassword && isset($user->password)) {
adfb459c 55
bb78e249 56 // Check password toward the password policy.
adfb459c
JM
57 if (!check_password_policy($user->password, $errmsg)) {
58 throw new moodle_exception($errmsg);
59 }
60
61 $userpassword = $user->password;
62 unset($user->password);
63 }
bd0f26bd 64
7a067206
SL
65 // Apply default values for user preferences that are stored in users table.
66 if (!isset($user->calendartype)) {
8bf0f207
MN
67 $user->calendartype = $CFG->calendartype;
68 }
69
9f7379e9
MG
70 if (!isset($user->maildisplay)) {
71 $user->maildisplay = $CFG->defaultpreference_maildisplay;
72 }
73 if (!isset($user->mailformat)) {
74 $user->mailformat = $CFG->defaultpreference_mailformat;
75 }
76 if (!isset($user->maildigest)) {
77 $user->maildigest = $CFG->defaultpreference_maildigest;
78 }
79 if (!isset($user->autosubscribe)) {
80 $user->autosubscribe = $CFG->defaultpreference_autosubscribe;
81 }
82 if (!isset($user->trackforums)) {
83 $user->trackforums = $CFG->defaultpreference_trackforums;
84 }
7e670032
DM
85 if (!isset($user->lang)) {
86 $user->lang = $CFG->lang;
87 }
9f7379e9 88
fb79269b 89 $user->timecreated = time();
bd0f26bd 90 $user->timemodified = $user->timecreated;
fb79269b 91
7a067206
SL
92 // Validate user data object.
93 $uservalidation = core_user::validate($user);
94 if ($uservalidation !== true) {
95 foreach ($uservalidation as $field => $message) {
96 debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
97 $user->$field = core_user::clean_field($user->$field, $field);
98 }
99 }
100
bb78e249 101 // Insert the user into the database.
fb79269b 102 $newuserid = $DB->insert_record('user', $user);
103
bb78e249
RT
104 // Create USER context for this user.
105 $usercontext = context_user::instance($newuserid);
b6dcb7d9 106
bb78e249 107 // Update user password if necessary.
adfb459c 108 if (isset($userpassword)) {
bb78e249
RT
109 // Get full database user row, in case auth is default.
110 $newuser = $DB->get_record('user', array('id' => $newuserid));
adfb459c
JM
111 $authplugin = get_auth_plugin($newuser->auth);
112 $authplugin->user_update_password($newuser, $userpassword);
113 }
114
2b55cb1b
RT
115 // Trigger event If required.
116 if ($triggerevent) {
117 \core\event\user_created::create_from_userid($newuserid)->trigger();
118 }
adfb459c 119
fb79269b 120 return $newuserid;
fb79269b 121}
122
123/**
124 * Update a user with a user object (will compare against the ID)
adfb459c 125 *
a2ed6e69 126 * @throws moodle_exception
bb78e249
RT
127 * @param stdClass $user the user to update
128 * @param bool $updatepassword if true, authentication plugin will update password.
2b55cb1b
RT
129 * @param bool $triggerevent set false if user_updated event should not be triggred.
130 * This will not affect user_password_updated event triggering.
fb79269b 131 */
2b55cb1b 132function user_update_user($user, $updatepassword = true, $triggerevent = true) {
fb79269b 133 global $DB;
bd0f26bd 134
a2ed6e69 135 // Set the timecreate field to the current time.
bd0f26bd 136 if (!is_object($user)) {
8bf0f207 137 $user = (object) $user;
bd0f26bd 138 }
adfb459c 139
a2ed6e69 140 // Check username.
45b4464c 141 if (isset($user->username)) {
2f1e464a 142 if ($user->username !== core_text::strtolower($user->username)) {
45b4464c
JM
143 throw new moodle_exception('usernamelowercase');
144 } else {
7a067206 145 if ($user->username !== core_user::clean_field($user->username, 'username')) {
45b4464c
JM
146 throw new moodle_exception('invalidusername');
147 }
148 }
149 }
150
bb78e249
RT
151 // Unset password here, for updating later, if password update is required.
152 if ($updatepassword && isset($user->password)) {
adfb459c 153
a2ed6e69 154 // Check password toward the password policy.
adfb459c
JM
155 if (!check_password_policy($user->password, $errmsg)) {
156 throw new moodle_exception($errmsg);
157 }
158
9e63c0ff
FS
159 $passwd = $user->password;
160 unset($user->password);
161 }
bd0f26bd 162
8bf0f207 163 // Make sure calendartype, if set, is valid.
7a067206 164 if (empty($user->calendartype)) {
8bf0f207
MN
165 // Unset this variable, must be an empty string, which we do not want to update the calendartype to.
166 unset($user->calendartype);
167 }
168
bd0f26bd 169 $user->timemodified = time();
7a067206
SL
170
171 // Validate user data object.
172 $uservalidation = core_user::validate($user);
173 if ($uservalidation !== true) {
174 foreach ($uservalidation as $field => $message) {
175 debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
176 $user->$field = core_user::clean_field($user->$field, $field);
177 }
178 }
179
fb79269b 180 $DB->update_record('user', $user);
b6dcb7d9 181
bb78e249
RT
182 if ($updatepassword) {
183 // Get full user record.
184 $updateduser = $DB->get_record('user', array('id' => $user->id));
9e63c0ff 185
a2ed6e69 186 // If password was set, then update its hash.
bb78e249
RT
187 if (isset($passwd)) {
188 $authplugin = get_auth_plugin($updateduser->auth);
189 if ($authplugin->can_change_password()) {
190 $authplugin->user_update_password($updateduser, $passwd);
191 }
adfb459c
JM
192 }
193 }
2b55cb1b
RT
194 // Trigger event if required.
195 if ($triggerevent) {
196 \core\event\user_updated::create_from_userid($user->id)->trigger();
197 }
adfb459c 198}
fb79269b 199
200/**
201 * Marks user deleted in internal user database and notifies the auth plugin.
202 * Also unenrols user from all roles and does other cleanup.
203 *
204 * @todo Decide if this transaction is really needed (look for internal TODO:)
205 * @param object $user Userobject before delete (without system magic quotes)
206 * @return boolean success
207 */
208function user_delete_user($user) {
45fb2cf8 209 return delete_user($user);
fb79269b 210}
211
212/**
213 * Get users by id
fb79269b 214 *
a2ed6e69
SH
215 * @param array $userids id of users to retrieve
216 * @return array
fb79269b 217 */
218function user_get_users_by_id($userids) {
219 global $DB;
220 return $DB->get_records_list('user', 'id', $userids);
221}
b1627a92 222
61c8e0d7
FM
223/**
224 * Returns the list of default 'displayable' fields
225 *
226 * Contains database field names but also names used to generate information, such as enrolledcourses
227 *
228 * @return array of user fields
229 */
230function user_get_default_fields() {
231 return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email',
232 'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo', 'aim', 'msn', 'department',
233 'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
234 'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
235 'city', 'url', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
236 'groups', 'roles', 'preferences', 'enrolledcourses'
237 );
238}
01479290
DC
239
240/**
241 *
a2ed6e69 242 * Give user record from mdl_user, build an array contains all user details.
93ce0e82
JM
243 *
244 * Warning: description file urls are 'webservice/pluginfile.php' is use.
245 * it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
246 *
a2ed6e69 247 * @throws moodle_exception
01479290 248 * @param stdClass $user user record from mdl_user
01479290 249 * @param stdClass $course moodle course
ad7612f5 250 * @param array $userfields required fields
d6731600 251 * @return array|null
01479290 252 */
ad7612f5 253function user_get_user_details($user, $course = null, array $userfields = array()) {
1f7273af 254 global $USER, $DB, $CFG, $PAGE;
a2ed6e69
SH
255 require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library.
256 require_once($CFG->dirroot . "/lib/filelib.php"); // File handling on description and friends.
01479290 257
61c8e0d7 258 $defaultfields = user_get_default_fields();
ad7612f5
DC
259
260 if (empty($userfields)) {
261 $userfields = $defaultfields;
262 }
263
264 foreach ($userfields as $thefield) {
265 if (!in_array($thefield, $defaultfields)) {
266 throw new moodle_exception('invaliduserfield', 'error', '', $thefield);
267 }
268 }
269
a2ed6e69 270 // Make sure id and fullname are included.
ad7612f5
DC
271 if (!in_array('id', $userfields)) {
272 $userfields[] = 'id';
273 }
274
275 if (!in_array('fullname', $userfields)) {
276 $userfields[] = 'fullname';
277 }
278
01479290 279 if (!empty($course)) {
43731030
FM
280 $context = context_course::instance($course->id);
281 $usercontext = context_user::instance($user->id);
1e539f64 282 $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
01479290 283 } else {
43731030 284 $context = context_user::instance($user->id);
01479290 285 $usercontext = $context;
1e539f64 286 $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
01479290
DC
287 }
288
289 $currentuser = ($user->id == $USER->id);
290 $isadmin = is_siteadmin($USER);
291
48a7b182
JM
292 $showuseridentityfields = get_extra_user_fields($context);
293
01479290
DC
294 if (!empty($course)) {
295 $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
296 } else {
297 $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
298 }
86477112 299 $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
01479290
DC
300 if (!empty($course)) {
301 $canviewuseremail = has_capability('moodle/course:useremail', $context);
302 } else {
303 $canviewuseremail = false;
304 }
a2ed6e69 305 $cannotviewdescription = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
01479290
DC
306 if (!empty($course)) {
307 $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
308 } else {
309 $canaccessallgroups = false;
310 }
311
312 if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) {
a2ed6e69 313 // Skip this user details.
01479290
DC
314 return null;
315 }
316
317 $userdetails = array();
318 $userdetails['id'] = $user->id;
319
07d37084
DM
320 if (in_array('username', $userfields)) {
321 if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
322 $userdetails['username'] = $user->username;
323 }
01479290
DC
324 }
325 if ($isadmin or $canviewfullnames) {
ad7612f5
DC
326 if (in_array('firstname', $userfields)) {
327 $userdetails['firstname'] = $user->firstname;
328 }
329 if (in_array('lastname', $userfields)) {
330 $userdetails['lastname'] = $user->lastname;
331 }
01479290
DC
332 }
333 $userdetails['fullname'] = fullname($user);
334
ad7612f5
DC
335 if (in_array('customfields', $userfields)) {
336 $fields = $DB->get_recordset_sql("SELECT f.*
337 FROM {user_info_field} f
338 JOIN {user_info_category} c
339 ON f.categoryid=c.id
340 ORDER BY c.sortorder ASC, f.sortorder ASC");
341 $userdetails['customfields'] = array();
342 foreach ($fields as $field) {
343 require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
344 $newfield = 'profile_field_'.$field->datatype;
345 $formfield = new $newfield($field->id, $user->id);
346 if ($formfield->is_visible() and !$formfield->is_empty()) {
f804c730 347
e96e66aa
EL
348 // TODO: Part of MDL-50728, this conditional coding must be moved to
349 // proper profile fields API so they are self-contained.
f804c730
JL
350 // We only use display_data in fields that require text formatting.
351 if ($field->datatype == 'text' or $field->datatype == 'textarea') {
352 $fieldvalue = $formfield->display_data();
353 } else {
354 // Cases: datetime, checkbox and menu.
355 $fieldvalue = $formfield->data;
356 }
357
ad7612f5 358 $userdetails['customfields'][] =
f804c730 359 array('name' => $formfield->field->name, 'value' => $fieldvalue,
ad7612f5
DC
360 'type' => $field->datatype, 'shortname' => $formfield->field->shortname);
361 }
362 }
363 $fields->close();
a2ed6e69 364 // Unset customfields if it's empty.
ad7612f5
DC
365 if (empty($userdetails['customfields'])) {
366 unset($userdetails['customfields']);
01479290 367 }
01479290
DC
368 }
369
a2ed6e69 370 // Profile image.
ad7612f5 371 if (in_array('profileimageurl', $userfields)) {
1f7273af
DP
372 $userpicture = new user_picture($user);
373 $userpicture->size = 1; // Size f1.
374 $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
ad7612f5
DC
375 }
376 if (in_array('profileimageurlsmall', $userfields)) {
1f7273af
DP
377 if (!isset($userpicture)) {
378 $userpicture = new user_picture($user);
379 }
380 $userpicture->size = 0; // Size f2.
381 $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
ad7612f5 382 }
01479290 383
a2ed6e69 384 // Hidden user field.
01479290
DC
385 if ($canviewhiddenuserfields) {
386 $hiddenfields = array();
a2ed6e69
SH
387 // Address, phone1 and phone2 not appears in hidden fields list but require viewhiddenfields capability
388 // according to user/profile.php.
ad7612f5 389 if ($user->address && in_array('address', $userfields)) {
01479290
DC
390 $userdetails['address'] = $user->address;
391 }
01479290
DC
392 } else {
393 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
394 }
395
48a7b182 396 if ($user->phone1 && in_array('phone1', $userfields) &&
58f739c5 397 (in_array('phone1', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
398 $userdetails['phone1'] = $user->phone1;
399 }
400 if ($user->phone2 && in_array('phone2', $userfields) &&
58f739c5 401 (in_array('phone2', $showuseridentityfields) or $canviewhiddenuserfields)) {
48a7b182
JM
402 $userdetails['phone2'] = $user->phone2;
403 }
404
acf64596
JM
405 if (isset($user->description) &&
406 ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
407 if (in_array('description', $userfields)) {
408 // Always return the descriptionformat if description is requested.
409 list($userdetails['description'], $userdetails['descriptionformat']) =
410 external_format_text($user->description, $user->descriptionformat,
411 $usercontext->id, 'user', 'profile', null);
01479290
DC
412 }
413 }
414
ad7612f5 415 if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
01479290
DC
416 $userdetails['country'] = $user->country;
417 }
418
ad7612f5 419 if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
01479290
DC
420 $userdetails['city'] = $user->city;
421 }
422
ad7612f5 423 if (in_array('url', $userfields) && $user->url && (!isset($hiddenfields['webpage']) or $isadmin)) {
01479290
DC
424 $url = $user->url;
425 if (strpos($user->url, '://') === false) {
426 $url = 'http://'. $url;
427 }
428 $user->url = clean_param($user->url, PARAM_URL);
429 $userdetails['url'] = $user->url;
430 }
431
ad7612f5 432 if (in_array('icq', $userfields) && $user->icq && (!isset($hiddenfields['icqnumber']) or $isadmin)) {
01479290
DC
433 $userdetails['icq'] = $user->icq;
434 }
435
ad7612f5 436 if (in_array('skype', $userfields) && $user->skype && (!isset($hiddenfields['skypeid']) or $isadmin)) {
01479290
DC
437 $userdetails['skype'] = $user->skype;
438 }
ad7612f5 439 if (in_array('yahoo', $userfields) && $user->yahoo && (!isset($hiddenfields['yahooid']) or $isadmin)) {
01479290
DC
440 $userdetails['yahoo'] = $user->yahoo;
441 }
ad7612f5 442 if (in_array('aim', $userfields) && $user->aim && (!isset($hiddenfields['aimid']) or $isadmin)) {
01479290
DC
443 $userdetails['aim'] = $user->aim;
444 }
ad7612f5 445 if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
01479290
DC
446 $userdetails['msn'] = $user->msn;
447 }
448
ad7612f5 449 if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
01479290
DC
450 if ($user->firstaccess) {
451 $userdetails['firstaccess'] = $user->firstaccess;
452 } else {
453 $userdetails['firstaccess'] = 0;
454 }
455 }
ad7612f5 456 if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
01479290
DC
457 if ($user->lastaccess) {
458 $userdetails['lastaccess'] = $user->lastaccess;
459 } else {
460 $userdetails['lastaccess'] = 0;
461 }
462 }
463
a2ed6e69
SH
464 if (in_array('email', $userfields) && ($isadmin // The admin is allowed the users email.
465 or $currentuser // Of course the current user is as well.
466 or $canviewuseremail // This is a capability in course context, it will be false in usercontext.
58f739c5 467 or in_array('email', $showuseridentityfields)
01479290 468 or $user->maildisplay == 1
ca774c94 469 or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER)))) {
90e30b96 470 $userdetails['email'] = $user->email;
01479290
DC
471 }
472
c4e868d5 473 if (in_array('interests', $userfields)) {
e11d7380 474 $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
c4e868d5
MG
475 if ($interests) {
476 $userdetails['interests'] = join(', ', $interests);
01479290
DC
477 }
478 }
479
a2ed6e69 480 // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
07d37084
DM
481 if (in_array('idnumber', $userfields) && $user->idnumber) {
482 if (in_array('idnumber', $showuseridentityfields) or $currentuser or
483 has_capability('moodle/user:viewalldetails', $context)) {
3a3f3b22
CW
484 $userdetails['idnumber'] = $user->idnumber;
485 }
48a7b182 486 }
07d37084
DM
487 if (in_array('institution', $userfields) && $user->institution) {
488 if (in_array('institution', $showuseridentityfields) or $currentuser or
489 has_capability('moodle/user:viewalldetails', $context)) {
01479290
DC
490 $userdetails['institution'] = $user->institution;
491 }
48a7b182 492 }
07d37084
DM
493 // Isset because it's ok to have department 0.
494 if (in_array('department', $userfields) && isset($user->department)) {
495 if (in_array('department', $showuseridentityfields) or $currentuser or
496 has_capability('moodle/user:viewalldetails', $context)) {
01479290
DC
497 $userdetails['department'] = $user->department;
498 }
499 }
500
ad7612f5 501 if (in_array('roles', $userfields) && !empty($course)) {
a2ed6e69 502 // Not a big secret.
01479290
DC
503 $roles = get_user_roles($context, $user->id, false);
504 $userdetails['roles'] = array();
505 foreach ($roles as $role) {
506 $userdetails['roles'][] = array(
507 'roleid' => $role->roleid,
508 'name' => $role->name,
509 'shortname' => $role->shortname,
510 'sortorder' => $role->sortorder
511 );
512 }
513 }
514
a2ed6e69 515 // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
ad7612f5 516 if (in_array('groups', $userfields) && !empty($course) && $canaccessallgroups) {
93ce0e82
JM
517 $usergroups = groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid,
518 'g.id, g.name,g.description,g.descriptionformat');
01479290
DC
519 $userdetails['groups'] = array();
520 foreach ($usergroups as $group) {
93ce0e82
JM
521 list($group->description, $group->descriptionformat) =
522 external_format_text($group->description, $group->descriptionformat,
523 $context->id, 'group', 'description', $group->id);
a2ed6e69
SH
524 $userdetails['groups'][] = array('id' => $group->id, 'name' => $group->name,
525 'description' => $group->description, 'descriptionformat' => $group->descriptionformat);
01479290
DC
526 }
527 }
a2ed6e69 528 // List of courses where the user is enrolled.
ad7612f5 529 if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
01479290
DC
530 $enrolledcourses = array();
531 if ($mycourses = enrol_get_users_courses($user->id, true)) {
532 foreach ($mycourses as $mycourse) {
533 if ($mycourse->category) {
43731030 534 $coursecontext = context_course::instance($mycourse->id);
01479290
DC
535 $enrolledcourse = array();
536 $enrolledcourse['id'] = $mycourse->id;
43ff71a0 537 $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
8ebbb06a 538 $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
01479290
DC
539 $enrolledcourses[] = $enrolledcourse;
540 }
541 }
542 $userdetails['enrolledcourses'] = $enrolledcourses;
543 }
544 }
545
a2ed6e69 546 // User preferences.
ad7612f5 547 if (in_array('preferences', $userfields) && $currentuser) {
01479290
DC
548 $preferences = array();
549 $userpreferences = get_user_preferences();
a2ed6e69 550 foreach ($userpreferences as $prefname => $prefvalue) {
01479290 551 $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
a2ed6e69
SH
552 }
553 $userdetails['preferences'] = $preferences;
01479290 554 }
ad7612f5 555
01479290
DC
556 return $userdetails;
557}
558
86477112
FS
559/**
560 * Tries to obtain user details, either recurring directly to the user's system profile
c70b9853 561 * or through one of the user's course enrollments (course profile).
86477112 562 *
a2ed6e69 563 * @param stdClass $user The user.
c70b9853 564 * @return array if unsuccessful or the allowed user details.
86477112
FS
565 */
566function user_get_user_details_courses($user) {
567 global $USER;
568 $userdetails = null;
569
a2ed6e69 570 // Get the courses that the user is enrolled in (only active).
86477112
FS
571 $courses = enrol_get_users_courses($user->id, true);
572
c70b9853
JM
573 $systemprofile = false;
574 if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
575 $systemprofile = true;
576 }
86477112 577
c70b9853 578 // Try using system profile.
86477112
FS
579 if ($systemprofile) {
580 $userdetails = user_get_user_details($user, null);
581 } else {
c70b9853 582 // Try through course profile.
86477112 583 foreach ($courses as $course) {
854859e1 584 if (can_view_user_details_cap($user, $course) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
86477112
FS
585 $userdetails = user_get_user_details($user, $course);
586 }
587 }
588 }
589
590 return $userdetails;
591}
592
593/**
c70b9853
JM
594 * Check if $USER have the necessary capabilities to obtain user details.
595 *
a2ed6e69
SH
596 * @param stdClass $user
597 * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
c70b9853 598 * @return bool true if $USER can view user details.
86477112
FS
599 */
600function can_view_user_details_cap($user, $course = null) {
c70b9853 601 // Check $USER has the capability to view the user details at user context.
bea86e84 602 $usercontext = context_user::instance($user->id);
c70b9853
JM
603 $result = has_capability('moodle/user:viewdetails', $usercontext);
604 // Otherwise can $USER see them at course context.
605 if (!$result && !empty($course)) {
bea86e84 606 $context = context_course::instance($course->id);
c70b9853 607 $result = has_capability('moodle/user:viewdetails', $context);
86477112
FS
608 }
609 return $result;
610}
611
b1627a92
DC
612/**
613 * Return a list of page types
614 * @param string $pagetype current page type
615 * @param stdClass $parentcontext Block's parent context
616 * @param stdClass $currentcontext Current context of block
a2ed6e69 617 * @return array
b1627a92 618 */
b38e2e28 619function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
a2ed6e69 620 return array('user-profile' => get_string('page-user-profile', 'pagetype'));
d6731600 621}
52dc1de7
AA
622
623/**
624 * Count the number of failed login attempts for the given user, since last successful login.
625 *
626 * @param int|stdclass $user user id or object.
627 * @param bool $reset Resets failed login count, if set to true.
628 *
629 * @return int number of failed login attempts since the last successful login.
630 */
631function user_count_login_failures($user, $reset = true) {
632 global $DB;
633
634 if (!is_object($user)) {
635 $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
636 }
637 if ($user->deleted) {
638 // Deleted user, nothing to do.
639 return 0;
640 }
641 $count = get_user_preferences('login_failed_count_since_success', 0, $user);
642 if ($reset) {
643 set_user_preference('login_failed_count_since_success', 0, $user);
644 }
645 return $count;
646}
647
b865a3e2 648/**
22893d6f
JC
649 * Converts a string into a flat array of menu items, where each menu items is a
650 * stdClass with fields type, url, title, pix, and imgsrc.
b865a3e2
JC
651 *
652 * @param string $text the menu items definition
653 * @param moodle_page $page the current page
654 * @return array
655 */
656function user_convert_text_to_menu_items($text, $page) {
657 global $OUTPUT, $CFG;
658
659 $lines = explode("\n", $text);
660 $items = array();
661 $lastchild = null;
662 $lastdepth = null;
663 $lastsort = 0;
088fcf90 664 $children = array();
b865a3e2
JC
665 foreach ($lines as $line) {
666 $line = trim($line);
667 $bits = explode('|', $line, 3);
22893d6f
JC
668 $itemtype = 'link';
669 if (preg_match("/^#+$/", $line)) {
670 $itemtype = 'divider';
671 } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
b865a3e2
JC
672 // Every item must have a name to be valid.
673 continue;
674 } else {
675 $bits[0] = ltrim($bits[0], '-');
676 }
677
678 // Create the child.
679 $child = new stdClass();
22893d6f
JC
680 $child->itemtype = $itemtype;
681 if ($itemtype === 'divider') {
682 // Add the divider to the list of children and skip link
683 // processing.
684 $children[] = $child;
685 continue;
686 }
b865a3e2
JC
687
688 // Name processing.
689 $namebits = explode(',', $bits[0], 2);
690 if (count($namebits) == 2) {
06c27531
AN
691 // Check the validity of the identifier part of the string.
692 if (clean_param($namebits[0], PARAM_STRINGID) !== '') {
693 // Treat this as a language string.
694 $child->title = get_string($namebits[0], $namebits[1]);
d7dbef73 695 $child->titleidentifier = implode(',', $namebits);
06c27531
AN
696 }
697 }
698 if (empty($child->title)) {
b865a3e2
JC
699 // Use it as is, don't even clean it.
700 $child->title = $bits[0];
d7dbef73 701 $child->titleidentifier = str_replace(" ", "-", $bits[0]);
b865a3e2
JC
702 }
703
704 // URL processing.
705 if (!array_key_exists(1, $bits) or empty($bits[1])) {
22893d6f 706 // Set the url to null, and set the itemtype to invalid.
b865a3e2 707 $bits[1] = null;
22893d6f 708 $child->itemtype = "invalid";
b865a3e2 709 } else {
4887d152 710 // Nasty hack to replace the grades with the direct url.
c9451960
AG
711 if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
712 $bits[1] = user_mygrades_url();
713 }
714
b865a3e2
JC
715 // Make sure the url is a moodle url.
716 $bits[1] = new moodle_url(trim($bits[1]));
717 }
718 $child->url = $bits[1];
719
720 // PIX processing.
721 $pixpath = "t/edit";
722 if (!array_key_exists(2, $bits) or empty($bits[2])) {
723 // Use the default.
724 $child->pix = $pixpath;
725 } else {
726 // Check for the specified image existing.
727 $pixpath = "t/" . $bits[2];
728 if ($page->theme->resolve_image_location($pixpath, 'moodle', true)) {
729 // Use the image.
730 $child->pix = $pixpath;
731 } else {
732 // Treat it like a URL.
733 $child->pix = null;
734 $child->imgsrc = $bits[2];
735 }
736 }
737
738 // Add this child to the list of children.
739 $children[] = $child;
740 }
741 return $children;
742}
743
6da0e4cf
JC
744/**
745 * Get a list of essential user navigation items.
746 *
747 * @param stdclass $user user object.
748 * @param moodle_page $page page object.
9dcd5035
DB
749 * @param array $options associative array.
750 * options are:
751 * - avatarsize=35 (size of avatar image)
6da0e4cf
JC
752 * @return stdClass $returnobj navigation information object, where:
753 *
754 * $returnobj->navitems array array of links where each link is a
755 * stdClass with fields url, title, and
756 * pix
757 * $returnobj->metadata array array of useful user metadata to be
758 * used when constructing navigation;
759 * fields include:
760 *
761 * ROLE FIELDS
762 * asotherrole bool whether viewing as another role
763 * rolename string name of the role
764 *
765 * USER FIELDS
766 * These fields are for the currently-logged in user, or for
767 * the user that the real user is currently logged in as.
768 *
769 * userid int the id of the user in question
770 * userfullname string the user's full name
771 * userprofileurl moodle_url the url of the user's profile
772 * useravatar string a HTML fragment - the rendered
773 * user_picture for this user
774 * userloginfail string an error string denoting the number
775 * of login failures since last login
776 *
777 * "REAL USER" FIELDS
778 * These fields are for when asotheruser is true, and
779 * correspond to the underlying "real user".
780 *
781 * asotheruser bool whether viewing as another user
782 * realuserid int the id of the user in question
783 * realuserfullname string the user's full name
784 * realuserprofileurl moodle_url the url of the user's profile
785 * realuseravatar string a HTML fragment - the rendered
786 * user_picture for this user
787 *
788 * MNET PROVIDER FIELDS
789 * asmnetuser bool whether viewing as a user from an
790 * MNet provider
791 * mnetidprovidername string name of the MNet provider
792 * mnetidproviderwwwroot string URL of the MNet provider
793 */
c68b3206 794function user_get_user_navigation_info($user, $page, $options = array()) {
6da0e4cf
JC
795 global $OUTPUT, $DB, $SESSION, $CFG;
796
797 $returnobject = new stdClass();
798 $returnobject->navitems = array();
799 $returnobject->metadata = array();
800
801 $course = $page->course;
802
803 // Query the environment.
804 $context = context_course::instance($course->id);
805
806 // Get basic user metadata.
807 $returnobject->metadata['userid'] = $user->id;
808 $returnobject->metadata['userfullname'] = fullname($user, true);
809 $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
810 'id' => $user->id
811 ));
9dcd5035
DB
812
813 $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
814 if (!empty($options['avatarsize'])) {
815 $avataroptions['size'] = $options['avatarsize'];
816 }
6da0e4cf 817 $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
9dcd5035 818 $user, $avataroptions
6da0e4cf
JC
819 );
820 // Build a list of items for a regular user.
821
822 // Query MNet status.
823 if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
824 $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
825 $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
826 $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
827 }
828
829 // Did the user just log in?
830 if (isset($SESSION->justloggedin)) {
831 // Don't unset this flag as login_info still needs it.
832 if (!empty($CFG->displayloginfailures)) {
833 // We're already in /user/lib.php, so we don't need to include.
834 if ($count = user_count_login_failures($user)) {
835
836 // Get login failures string.
837 $a = new stdClass();
838 $a->attempts = html_writer::tag('span', $count, array('class' => 'value'));
839 $returnobject->metadata['userloginfail'] =
840 get_string('failedloginattempts', '', $a);
841
842 }
843 }
844 }
845
81d7de1a 846 // Links: Dashboard.
6da0e4cf 847 $myhome = new stdClass();
22893d6f 848 $myhome->itemtype = 'link';
6da0e4cf
JC
849 $myhome->url = new moodle_url('/my/');
850 $myhome->title = get_string('mymoodle', 'admin');
d7dbef73 851 $myhome->titleidentifier = 'mymoodle,admin';
6da0e4cf
JC
852 $myhome->pix = "i/course";
853 $returnobject->navitems[] = $myhome;
854
855 // Links: My Profile.
856 $myprofile = new stdClass();
22893d6f 857 $myprofile->itemtype = 'link';
6da0e4cf 858 $myprofile->url = new moodle_url('/user/profile.php', array('id' => $user->id));
4887d152 859 $myprofile->title = get_string('profile');
d7dbef73 860 $myprofile->titleidentifier = 'profile,moodle';
6da0e4cf
JC
861 $myprofile->pix = "i/user";
862 $returnobject->navitems[] = $myprofile;
863
864 // Links: Role-return or logout link.
865 $lastobj = null;
866 $buildlogout = true;
867 $returnobject->metadata['asotherrole'] = false;
868 if (is_role_switched($course->id)) {
869 if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
870 // Build role-return link instead of logout link.
871 $rolereturn = new stdClass();
22893d6f 872 $rolereturn->itemtype = 'link';
6da0e4cf
JC
873 $rolereturn->url = new moodle_url('/course/switchrole.php', array(
874 'id' => $course->id,
875 'sesskey' => sesskey(),
876 'switchrole' => 0,
877 'returnurl' => $page->url->out_as_local_url(false)
878 ));
879 $rolereturn->pix = "a/logout";
880 $rolereturn->title = get_string('switchrolereturn');
d7dbef73 881 $rolereturn->titleidentifier = 'switchrolereturn,moodle';
6da0e4cf
JC
882 $lastobj = $rolereturn;
883
884 $returnobject->metadata['asotherrole'] = true;
885 $returnobject->metadata['rolename'] = role_get_name($role, $context);
886
887 $buildlogout = false;
888 }
889 }
890
891 if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
892 $realuser = \core\session\manager::get_realuser();
893
894 // Save values for the real user, as $user will be full of data for the
895 // user the user is disguised as.
896 $returnobject->metadata['realuserid'] = $realuser->id;
897 $returnobject->metadata['realuserfullname'] = fullname($realuser, true);
898 $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', array(
899 'id' => $realuser->id
900 ));
c68b3206 901 $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
6da0e4cf
JC
902
903 // Build a user-revert link.
904 $userrevert = new stdClass();
22893d6f 905 $userrevert->itemtype = 'link';
6da0e4cf
JC
906 $userrevert->url = new moodle_url('/course/loginas.php', array(
907 'id' => $course->id,
908 'sesskey' => sesskey()
909 ));
910 $userrevert->pix = "a/logout";
911 $userrevert->title = get_string('logout');
d7dbef73 912 $userrevert->titleidentifier = 'logout,moodle';
6da0e4cf
JC
913 $lastobj = $userrevert;
914
915 $buildlogout = false;
916 }
917
918 if ($buildlogout) {
919 // Build a logout link.
920 $logout = new stdClass();
22893d6f 921 $logout->itemtype = 'link';
6da0e4cf
JC
922 $logout->url = new moodle_url('/login/logout.php', array('sesskey' => sesskey()));
923 $logout->pix = "a/logout";
924 $logout->title = get_string('logout');
d7dbef73 925 $logout->titleidentifier = 'logout,moodle';
6da0e4cf
JC
926 $lastobj = $logout;
927 }
928
b865a3e2
JC
929 // Before we add the last item (usually a logout link), add any
930 // custom-defined items.
931 $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
932 foreach ($customitems as $item) {
933 $returnobject->navitems[] = $item;
934 }
935
6da0e4cf
JC
936 // Add the last item to the list.
937 if (!is_null($lastobj)) {
938 $returnobject->navitems[] = $lastobj;
939 }
940
941 return $returnobject;
1d658535
PS
942}
943
944/**
945 * Add password to the list of used hashes for this user.
946 *
947 * This is supposed to be used from:
948 * 1/ change own password form
949 * 2/ password reset process
950 * 3/ user signup in auth plugins if password changing supported
951 *
952 * @param int $userid user id
953 * @param string $password plaintext password
954 * @return void
955 */
956function user_add_password_history($userid, $password) {
957 global $CFG, $DB;
958 require_once($CFG->libdir.'/password_compat/lib/password.php');
959
960 if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
961 return;
962 }
963
964 // Note: this is using separate code form normal password hashing because
965 // we need to have this under control in the future. Also the auth
966 // plugin might not store the passwords locally at all.
967
968 $record = new stdClass();
969 $record->userid = $userid;
970 $record->hash = password_hash($password, PASSWORD_DEFAULT);
971 $record->timecreated = time();
972 $DB->insert_record('user_password_history', $record);
973
974 $i = 0;
975 $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
976 foreach ($records as $record) {
977 $i++;
978 if ($i > $CFG->passwordreuselimit) {
979 $DB->delete_records('user_password_history', array('id' => $record->id));
980 }
981 }
982}
983
984/**
985 * Was this password used before on change or reset password page?
986 *
987 * The $CFG->passwordreuselimit setting determines
988 * how many times different password needs to be used
989 * before allowing previously used password again.
990 *
991 * @param int $userid user id
992 * @param string $password plaintext password
993 * @return bool true if password reused
994 */
995function user_is_previously_used_password($userid, $password) {
996 global $CFG, $DB;
997 require_once($CFG->libdir.'/password_compat/lib/password.php');
998
999 if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1000 return false;
1001 }
1002
1003 $reused = false;
1004
1005 $i = 0;
1006 $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1007 foreach ($records as $record) {
1008 $i++;
1009 if ($i > $CFG->passwordreuselimit) {
1010 $DB->delete_records('user_password_history', array('id' => $record->id));
1011 continue;
1012 }
1013 // NOTE: this is slow but we cannot compare the hashes directly any more.
1014 if (password_verify($password, $record->hash)) {
1015 $reused = true;
1016 }
1017 }
1018
1019 return $reused;
1020}
3221718e
JL
1021
1022/**
1023 * Remove a user device from the Moodle database (for PUSH notifications usually).
1024 *
1025 * @param string $uuid The device UUID.
1026 * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1027 * @return bool true if removed, false if the device didn't exists in the database
1028 * @since Moodle 2.9
1029 */
1030function user_remove_user_device($uuid, $appid = "") {
1031 global $DB, $USER;
1032
1033 $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1034 if (!empty($appid)) {
1035 $conditions['appid'] = $appid;
1036 }
1037
1038 if (!$DB->count_records('user_devices', $conditions)) {
1039 return false;
1040 }
1041
1042 $DB->delete_records('user_devices', $conditions);
1043
1044 return true;
1045}
0ff203b6
JL
1046
1047/**
1048 * Trigger user_list_viewed event.
1049 *
1050 * @param stdClass $course course object
1051 * @param stdClass $context course context object
1052 * @since Moodle 2.9
1053 */
1054function user_list_view($course, $context) {
1055
1056 $event = \core\event\user_list_viewed::create(array(
1057 'objectid' => $course->id,
1058 'courseid' => $course->id,
1059 'context' => $context,
1060 'other' => array(
1061 'courseshortname' => $course->shortname,
1062 'coursefullname' => $course->fullname
1063 )
1064 ));
1065 $event->trigger();
1066}
c9451960
AG
1067
1068/**
4887d152 1069 * Returns the url to use for the "Grades" link in the user navigation.
c9451960
AG
1070 *
1071 * @param int $userid The user's ID.
1072 * @param int $courseid The course ID if available.
4887d152 1073 * @return mixed A URL to be directed to for "Grades".
c9451960
AG
1074 */
1075function user_mygrades_url($userid = null, $courseid = SITEID) {
1076 global $CFG, $USER;
1077 $url = null;
1078 if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1079 if (isset($userid) && $USER->id != $userid) {
1080 // Send to the gradebook report.
1081 $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1082 array('id' => $courseid, 'userid' => $userid));
1083 } else {
1084 $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1085 }
1086 } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1087 && !empty($CFG->gradereport_mygradeurl)) {
1088 $url = $CFG->gradereport_mygradeurl;
1089 } else {
1090 $url = $CFG->wwwroot;
1091 }
1092 return $url;
1093}
66a43cd5
AG
1094
1095/**
1096 * Check if a user has the permission to viewdetails in a shared course's context.
1097 *
1098 * @param object $user The other user's details.
1099 * @param object $course Use this course to see if we have permission to see this user's profile.
1100 * @param context $usercontext The user context if available.
1101 * @return bool true for ability to view this user, else false.
1102 */
1103function user_can_view_profile($user, $course = null, $usercontext = null) {
1104 global $USER, $CFG;
1105
1106 if ($user->deleted) {
1107 return false;
1108 }
1109
1110 // If any of these four things, return true.
1111 // Number 1.
1112 if ($USER->id == $user->id) {
1113 return true;
1114 }
1115
1116 // Number 2.
1117 if (empty($CFG->forceloginforprofiles)) {
1118 return true;
1119 }
1120
1121 if (empty($usercontext)) {
1122 $usercontext = context_user::instance($user->id);
1123 }
1124 // Number 3.
1125 if (has_capability('moodle/user:viewdetails', $usercontext)) {
1126 return true;
1127 }
1128
1129 // Number 4.
1130 if (has_coursecontact_role($user->id)) {
1131 return true;
1132 }
1133
1134 if (isset($course)) {
1135 $sharedcourses = array($course);
1136 } else {
1137 $sharedcourses = enrol_get_shared_courses($USER->id, $user->id, true);
1138 }
1139 foreach ($sharedcourses as $sharedcourse) {
1140 $coursecontext = context_course::instance($sharedcourse->id);
1141 if (has_capability('moodle/user:viewdetails', $coursecontext)) {
1142 if (!groups_user_groups_visible($sharedcourse, $user->id)) {
1143 // Not a member of the same group.
1144 continue;
1145 }
1146 return true;
1147 }
1148 }
1149 return false;
1150}
c4e868d5
MG
1151
1152/**
1153 * Returns users tagged with a specified tag.
1154 *
1155 * @param core_tag_tag $tag
1156 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1157 * are displayed on the page and the per-page limit may be bigger
1158 * @param int $fromctx context id where the link was displayed, may be used by callbacks
1159 * to display items in the same context first
1160 * @param int $ctx context id where to search for records
1161 * @param bool $rec search in subcontexts as well
1162 * @param int $page 0-based number of page being displayed
1163 * @return \core_tag\output\tagindex
1164 */
1165function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1166 global $PAGE;
1167
1168 if ($ctx && $ctx != context_system::instance()->id) {
1169 $usercount = 0;
1170 } else {
1171 // Users can only be displayed in system context.
1172 $usercount = $tag->count_tagged_items('core', 'user',
1173 'it.deleted=:notdeleted', array('notdeleted' => 0));
1174 }
1175 $perpage = $exclusivemode ? 24 : 5;
1176 $content = '';
1177 $totalpages = ceil($usercount / $perpage);
1178
1179 if ($usercount) {
1180 $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage,
1181 'it.deleted=:notdeleted', array('notdeleted' => 0));
1182 $renderer = $PAGE->get_renderer('core', 'user');
1183 $content .= $renderer->user_list($userlist, $exclusivemode);
1184 }
1185
1186 return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1187 $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1188}