MDL-58220 oauth2: tighten the param types
[moodle.git] / lib / classes / user.php
CommitLineData
3bcf6b3c
RT
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * User class
19 *
20 * @package core
21 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27/**
28 * User class to access user details.
29 *
30 * @todo move api's from user/lib.php and depreciate old ones.
31 * @package core
32 * @copyright 2013 Rajesh Taneja <rajesh@moodle.com>
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class core_user {
36 /**
37 * No reply user id.
38 */
39 const NOREPLY_USER = -10;
40
41 /**
6c3ad77e 42 * Support user id.
3bcf6b3c
RT
43 */
44 const SUPPORT_USER = -20;
45
9715f61a
AG
46 /**
47 * Hide email address from everyone.
48 */
49 const MAILDISPLAY_HIDE = 0;
50
51 /**
52 * Display email address to everyone.
53 */
54 const MAILDISPLAY_EVERYONE = 1;
55
56 /**
57 * Display email address to course members only.
58 */
59 const MAILDISPLAY_COURSE_MEMBERS_ONLY = 2;
60
3bcf6b3c
RT
61 /** @var stdClass keep record of noreply user */
62 public static $noreplyuser = false;
63
64 /** @var stdClass keep record of support user */
65 public static $supportuser = false;
66
dccf9ca3
SL
67 /** @var array store user fields properties cache. */
68 protected static $propertiescache = null;
69
6e65554e
MG
70 /** @var array store user preferences cache. */
71 protected static $preferencescache = null;
72
3bcf6b3c
RT
73 /**
74 * Return user object from db or create noreply or support user,
75 * if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
76 * respectively. If userid is not found, then return false.
77 *
78 * @param int $userid user id
79 * @param string $fields A comma separated list of user fields to be returned, support and noreply user
80 * will not be filtered by this.
81 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
82 * IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
83 * MUST_EXIST means throw an exception if no user record or multiple records found.
84 * @return stdClass|bool user record if found, else false.
85 * @throws dml_exception if user record not found and respective $strictness is set.
86 */
87 public static function get_user($userid, $fields = '*', $strictness = IGNORE_MISSING) {
88 global $DB;
89
90 // If noreply user then create fake record and return.
91 switch ($userid) {
92 case self::NOREPLY_USER:
d1b30757 93 return self::get_noreply_user();
3bcf6b3c
RT
94 break;
95 case self::SUPPORT_USER:
d1b30757 96 return self::get_support_user();
3bcf6b3c
RT
97 break;
98 default:
99 return $DB->get_record('user', array('id' => $userid), $fields, $strictness);
100 }
101 }
102
28b592d5
DW
103 /**
104 * Return user object from db based on their email.
105 *
106 * @param string $email The email of the user searched.
107 * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
108 * @param int $mnethostid The id of the remote host.
109 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
110 * IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
111 * MUST_EXIST means throw an exception if no user record or multiple records found.
112 * @return stdClass|bool user record if found, else false.
113 * @throws dml_exception if user record not found and respective $strictness is set.
114 */
115 public static function get_user_by_email($email, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
116 global $DB, $CFG;
117
118 // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
119 if (empty($mnethostid)) {
120 // If empty, we restrict to local users.
121 $mnethostid = $CFG->mnet_localhost_id;
122 }
123
124 return $DB->get_record('user', array('email' => $email, 'mnethostid' => $mnethostid), $fields, $strictness);
125 }
2d35b7d3
GPL
126
127 /**
128 * Return user object from db based on their username.
129 *
130 * @param string $username The username of the user searched.
131 * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
132 * @param int $mnethostid The id of the remote host.
133 * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
134 * IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
135 * MUST_EXIST means throw an exception if no user record or multiple records found.
136 * @return stdClass|bool user record if found, else false.
137 * @throws dml_exception if user record not found and respective $strictness is set.
138 */
139 public static function get_user_by_username($username, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
140 global $DB, $CFG;
141
142 // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
143 if (empty($mnethostid)) {
144 // If empty, we restrict to local users.
145 $mnethostid = $CFG->mnet_localhost_id;
146 }
147
148 return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness);
149 }
150
3bcf6b3c
RT
151 /**
152 * Helper function to return dummy noreply user record.
153 *
154 * @return stdClass
155 */
156 protected static function get_dummy_user_record() {
157 global $CFG;
158
159 $dummyuser = new stdClass();
160 $dummyuser->id = self::NOREPLY_USER;
161 $dummyuser->email = $CFG->noreplyaddress;
162 $dummyuser->firstname = get_string('noreplyname');
163 $dummyuser->username = 'noreply';
164 $dummyuser->lastname = '';
165 $dummyuser->confirmed = 1;
166 $dummyuser->suspended = 0;
167 $dummyuser->deleted = 0;
168 $dummyuser->picture = 0;
169 $dummyuser->auth = 'manual';
170 $dummyuser->firstnamephonetic = '';
171 $dummyuser->lastnamephonetic = '';
172 $dummyuser->middlename = '';
173 $dummyuser->alternatename = '';
174 $dummyuser->imagealt = '';
175 return $dummyuser;
176 }
177
178 /**
179 * Return noreply user record, this is currently used in messaging
180 * system only for sending messages from noreply email.
181 * It will return record of $CFG->noreplyuserid if set else return dummy
182 * user object with hard-coded $user->emailstop = 1 so noreply can be sent to user.
183 *
184 * @return stdClass user record.
185 */
186 public static function get_noreply_user() {
187 global $CFG;
188
189 if (!empty(self::$noreplyuser)) {
190 return self::$noreplyuser;
191 }
192
193 // If noreply user is set then use it, else create one.
194 if (!empty($CFG->noreplyuserid)) {
195 self::$noreplyuser = self::get_user($CFG->noreplyuserid);
ec907228
SL
196 self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
197 return self::$noreplyuser;
198 } else {
199 // Do not cache the dummy user record to avoid language internationalization issues.
200 $noreplyuser = self::get_dummy_user_record();
201 $noreplyuser->maildisplay = '1'; // Show to all.
202 $noreplyuser->emailstop = 1;
203 return $noreplyuser;
3bcf6b3c 204 }
3bcf6b3c
RT
205 }
206
207 /**
208 * Return support user record, this is currently used in messaging
209 * system only for sending messages to support email.
210 * $CFG->supportuserid is set then returns user record
211 * $CFG->supportemail is set then return dummy record with $CFG->supportemail
212 * else return admin user record with hard-coded $user->emailstop = 0, so user
213 * gets support message.
214 *
215 * @return stdClass user record.
216 */
217 public static function get_support_user() {
218 global $CFG;
219
220 if (!empty(self::$supportuser)) {
221 return self::$supportuser;
222 }
223
224 // If custom support user is set then use it, else if supportemail is set then use it, else use noreply.
225 if (!empty($CFG->supportuserid)) {
226 self::$supportuser = self::get_user($CFG->supportuserid, '*', MUST_EXIST);
ec907228
SL
227 } else if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
228 // Try sending it to support email if support user is not set.
229 $supportuser = self::get_dummy_user_record();
230 $supportuser->id = self::SUPPORT_USER;
231 $supportuser->email = $CFG->supportemail;
6c3ad77e 232 if ($CFG->supportname) {
ec907228 233 $supportuser->firstname = $CFG->supportname;
6c3ad77e 234 }
ec907228
SL
235 $supportuser->username = 'support';
236 $supportuser->maildisplay = '1'; // Show to all.
237 // Unset emailstop to make sure support message is sent.
238 $supportuser->emailstop = 0;
239 return $supportuser;
3bcf6b3c
RT
240 }
241
242 // Send support msg to admin user if nothing is set above.
243 if (empty(self::$supportuser)) {
244 self::$supportuser = get_admin();
245 }
246
247 // Unset emailstop to make sure support message is sent.
248 self::$supportuser->emailstop = 0;
249 return self::$supportuser;
250 }
251
252 /**
253 * Reset self::$noreplyuser and self::$supportuser.
254 * This is only used by phpunit, and there is no other use case for this function.
255 * Please don't use it outside phpunit.
256 */
257 public static function reset_internal_users() {
258 if (PHPUNIT_TEST) {
259 self::$noreplyuser = false;
260 self::$supportuser = false;
261 } else {
262 debugging('reset_internal_users() should not be used outside phpunit.', DEBUG_DEVELOPER);
263 }
264 }
265
266 /**
267 * Return true is user id is greater than self::NOREPLY_USER and
6c3ad77e 268 * alternatively check db.
3bcf6b3c
RT
269 *
270 * @param int $userid user id.
271 * @param bool $checkdb if true userid will be checked in db. By default it's false, and
272 * userid is compared with NOREPLY_USER for performance.
273 * @return bool true is real user else false.
274 */
275 public static function is_real_user($userid, $checkdb = false) {
6c3ad77e
PS
276 global $DB;
277
3bcf6b3c
RT
278 if ($userid < 0) {
279 return false;
280 }
281 if ($checkdb) {
282 return $DB->record_exists('user', array('id' => $userid));
283 } else {
284 return true;
285 }
286 }
3961ebfb
JL
287
288 /**
289 * Check if the given user is an active user in the site.
290 *
291 * @param stdClass $user user object
292 * @param boolean $checksuspended whether to check if the user has the account suspended
293 * @param boolean $checknologin whether to check if the user uses the nologin auth method
294 * @throws moodle_exception
295 * @since Moodle 3.0
296 */
297 public static function require_active_user($user, $checksuspended = false, $checknologin = false) {
298
299 if (!self::is_real_user($user->id)) {
300 throw new moodle_exception('invaliduser', 'error');
301 }
302
303 if ($user->deleted) {
304 throw new moodle_exception('userdeleted');
305 }
306
307 if (empty($user->confirmed)) {
308 throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
309 }
310
311 if (isguestuser($user)) {
312 throw new moodle_exception('guestsarenotallowed', 'error');
313 }
314
315 if ($checksuspended and $user->suspended) {
316 throw new moodle_exception('suspended', 'auth');
317 }
318
319 if ($checknologin and $user->auth == 'nologin') {
320 throw new moodle_exception('suspended', 'auth');
321 }
322 }
dccf9ca3 323
5407c5b0
RS
324 /**
325 * Updates the provided users profile picture based upon the expected fields returned from the edit or edit_advanced forms.
326 *
327 * @param stdClass $usernew An object that contains some information about the user being updated
328 * @param array $filemanageroptions
329 * @return bool True if the user was updated, false if it stayed the same.
330 */
331 public static function update_picture(stdClass $usernew, $filemanageroptions = array()) {
332 global $CFG, $DB;
333 require_once("$CFG->libdir/gdlib.php");
334
335 $context = context_user::instance($usernew->id, MUST_EXIST);
336 $user = core_user::get_user($usernew->id, 'id, picture', MUST_EXIST);
337
338 $newpicture = $user->picture;
339 // Get file_storage to process files.
340 $fs = get_file_storage();
341 if (!empty($usernew->deletepicture)) {
342 // The user has chosen to delete the selected users picture.
343 $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
344 $newpicture = 0;
345
346 } else {
347 // Save newly uploaded file, this will avoid context mismatch for newly created users.
348 file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
349 if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
350 // Get file which was uploaded in draft area.
351 foreach ($iconfiles as $file) {
352 if (!$file->is_directory()) {
353 break;
354 }
355 }
356 // Copy file to temporary location and the send it for processing icon.
357 if ($iconfile = $file->copy_content_to_temp()) {
358 // There is a new image that has been uploaded.
359 // Process the new image and set the user to make use of it.
360 // NOTE: Uploaded images always take over Gravatar.
361 $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
362 // Delete temporary file.
363 @unlink($iconfile);
364 // Remove uploaded file.
365 $fs->delete_area_files($context->id, 'user', 'newicon');
366 } else {
367 // Something went wrong while creating temp file.
368 // Remove uploaded file.
369 $fs->delete_area_files($context->id, 'user', 'newicon');
370 return false;
371 }
372 }
373 }
374
375 if ($newpicture != $user->picture) {
376 $DB->set_field('user', 'picture', $newpicture, array('id' => $user->id));
377 return true;
378 } else {
379 return false;
380 }
381 }
382
383
384
dccf9ca3
SL
385 /**
386 * Definition of user profile fields and the expected parameter type for data validation.
387 *
4ce09314
SL
388 * array(
389 * 'property_name' => array( // The user property to be checked. Should match the field on the user table.
390 * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
391 * 'type' => PARAM_TYPE, // Expected parameter type of the user field.
392 * 'choices' => array(1, 2..) // An array of accepted values of the user field.
393 * 'default' => $CFG->setting // An default value for the field.
394 * )
395 * )
396 *
397 * The fields choices and default are optional.
398 *
dccf9ca3
SL
399 * @return void
400 */
401 protected static function fill_properties_cache() {
4ce09314 402 global $CFG;
dccf9ca3
SL
403 if (self::$propertiescache !== null) {
404 return;
405 }
406
407 // Array of user fields properties and expected parameters.
408 // Every new field on the user table should be added here otherwise it won't be validated.
409 $fields = array();
4ce09314
SL
410 $fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
411 $fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
412 $fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
413 $fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
414 $fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
415 $fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
416 $fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
417 $fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
418 $fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
419 $fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
420 $fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
421 $fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
422 $fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
423 $fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
424 $fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
425 $fields['icq'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
426 $fields['skype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
427 $fields['aim'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
428 $fields['yahoo'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
429 $fields['msn'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
430 $fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
431 $fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
432 $fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
433 $fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
434 $fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
435 $fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
436 $fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
437 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
438 $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->lang,
aa788b3c 439 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
4ce09314
SL
440 $fields['calendartype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
441 'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
442 $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
443 'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
16825c4e
FM
444 $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
445 'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
4ce09314
SL
446 $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
447 $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
448 $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
449 $fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
450 $fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
451 $fields['secret'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
452 $fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
453 $fields['url'] = array('type' => PARAM_URL, 'null' => NULL_NOT_ALLOWED);
454 $fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
455 $fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
456 $fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
457 'default' => $CFG->defaultpreference_mailformat);
458 $fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
459 'default' => $CFG->defaultpreference_maildigest);
460 $fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
461 'default' => $CFG->defaultpreference_maildisplay);
462 $fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
463 'default' => $CFG->defaultpreference_autosubscribe);
464 $fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
465 'default' => $CFG->defaultpreference_trackforums);
466 $fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
467 $fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
468 $fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
469 $fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
470 $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
471 $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
472 $fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
473 $fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
dccf9ca3
SL
474
475 self::$propertiescache = $fields;
476 }
477
478 /**
479 * Get properties of a user field.
480 *
481 * @param string $property property name to be retrieved.
482 * @throws coding_exception if the requested property name is invalid.
483 * @return array the property definition.
484 */
485 public static function get_property_definition($property) {
486
487 self::fill_properties_cache();
488
489 if (!array_key_exists($property, self::$propertiescache)) {
490 throw new coding_exception('Invalid property requested.');
491 }
492
493 return self::$propertiescache[$property];
494 }
495
4ce09314
SL
496 /**
497 * Validate user data.
498 *
499 * This method just validates each user field and return an array of errors. It doesn't clean the data,
500 * the methods clean() and clean_field() should be used for this purpose.
501 *
502 * @param stdClass|array $data user data object or array to be validated.
503 * @return array|true $errors array of errors found on the user object, true if the validation passed.
504 */
505 public static function validate($data) {
506 // Get all user profile fields definition.
507 self::fill_properties_cache();
508
509 foreach ($data as $property => $value) {
510 try {
511 if (isset(self::$propertiescache[$property])) {
512 validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
513 }
514 // Check that the value is part of a list of allowed values.
515 if (!empty(self::$propertiescache[$property]['choices']) &&
aa788b3c 516 !isset(self::$propertiescache[$property]['choices'][$value])) {
4ce09314
SL
517 throw new invalid_parameter_exception($value);
518 }
519 } catch (invalid_parameter_exception $e) {
520 $errors[$property] = $e->getMessage();
521 }
522 }
523
524 return empty($errors) ? true : $errors;
525 }
526
dccf9ca3
SL
527 /**
528 * Clean the properties cache.
529 *
530 * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
531 * Intended for use only for testing, phpunit calls this before every test.
532 */
533 public static function reset_caches() {
534 self::$propertiescache = null;
535 }
4ce09314
SL
536
537 /**
538 * Clean the user data.
539 *
540 * @param stdClass|array $user the user data to be validated against properties definition.
541 * @return stdClass $user the cleaned user data.
542 */
543 public static function clean_data($user) {
544 if (empty($user)) {
545 return $user;
546 }
547
548 foreach ($user as $field => $value) {
549 // Get the property parameter type and do the cleaning.
550 try {
aa788b3c 551 $user->$field = core_user::clean_field($value, $field);
4ce09314
SL
552 } catch (coding_exception $e) {
553 debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
554 }
555 }
556
557 return $user;
558 }
559
560 /**
561 * Clean a specific user field.
562 *
563 * @param string $data the user field data to be cleaned.
564 * @param string $field the user field name on the property definition cache.
565 * @return string the cleaned user data.
566 */
567 public static function clean_field($data, $field) {
568 if (empty($data) || empty($field)) {
569 return $data;
570 }
571
572 try {
573 $type = core_user::get_property_type($field);
574
575 if (isset(self::$propertiescache[$field]['choices'])) {
576 if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
577 if (isset(self::$propertiescache[$field]['default'])) {
578 $data = self::$propertiescache[$field]['default'];
579 } else {
580 $data = '';
581 }
aa788b3c
SL
582 } else {
583 return $data;
4ce09314
SL
584 }
585 } else {
586 $data = clean_param($data, $type);
587 }
588 } catch (coding_exception $e) {
589 debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
590 }
591
592 return $data;
593 }
594
595 /**
596 * Get the parameter type of the property.
597 *
598 * @param string $property property name to be retrieved.
599 * @throws coding_exception if the requested property name is invalid.
600 * @return int the property parameter type.
601 */
602 public static function get_property_type($property) {
603
604 self::fill_properties_cache();
605
606 if (!array_key_exists($property, self::$propertiescache)) {
607 throw new coding_exception('Invalid property requested: ' . $property);
608 }
609
610 return self::$propertiescache[$property]['type'];
611 }
612
613 /**
614 * Discover if the property is NULL_ALLOWED or NULL_NOT_ALLOWED.
615 *
616 * @param string $property property name to be retrieved.
617 * @throws coding_exception if the requested property name is invalid.
618 * @return bool true if the property is NULL_ALLOWED, false otherwise.
619 */
620 public static function get_property_null($property) {
621
622 self::fill_properties_cache();
623
624 if (!array_key_exists($property, self::$propertiescache)) {
625 throw new coding_exception('Invalid property requested: ' . $property);
626 }
627
628 return self::$propertiescache[$property]['null'];
629 }
630
631 /**
632 * Get the choices of the property.
633 *
634 * This is a helper method to validate a value against a list of acceptable choices.
16825c4e 635 * For instance: country, language, themes and etc.
4ce09314
SL
636 *
637 * @param string $property property name to be retrieved.
638 * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
639 * @return array the property parameter type.
640 */
641 public static function get_property_choices($property) {
642
643 self::fill_properties_cache();
644
645 if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
646 self::$propertiescache[$property])) {
647
648 throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
649 }
650
651 return self::$propertiescache[$property]['choices'];
652 }
653
654 /**
655 * Get the property default.
656 *
657 * This method gets the default value of a field (if exists).
658 *
659 * @param string $property property name to be retrieved.
660 * @throws coding_exception if the requested property name is invalid or if it does not has a default value.
661 * @return string the property default value.
662 */
663 public static function get_property_default($property) {
664
665 self::fill_properties_cache();
666
667 if (!array_key_exists($property, self::$propertiescache) || !isset(self::$propertiescache[$property]['default'])) {
668 throw new coding_exception('Invalid property requested, or the property does not has a default value.');
669 }
670
671 return self::$propertiescache[$property]['default'];
672 }
6e65554e
MG
673
674 /**
675 * Definition of updateable user preferences and rules for data and access validation.
676 *
677 * array(
678 * 'preferencename' => array( // Either exact preference name or a regular expression.
679 * 'null' => NULL_ALLOWED, // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
680 * 'type' => PARAM_TYPE, // Expected parameter type of the user field - mandatory
681 * 'choices' => array(1, 2..) // An array of accepted values of the user field - optional
682 * 'default' => $CFG->setting // An default value for the field - optional
683 * 'isregex' => false/true // Whether the name of the preference is a regular expression (default false).
684 * 'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
685 * // is allowed to modify this preference for given user.
686 * // If not specified core_user::default_preference_permission_check() will be assumed.
687 * 'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
688 * // accepts arguments ($value, $preferencename)
689 * )
690 * )
691 *
692 * @return void
693 */
694 protected static function fill_preferences_cache() {
695 if (self::$preferencescache !== null) {
696 return;
697 }
698
699 // Array of user preferences and expected types/values.
700 // Every preference that can be updated directly by user should be added here.
701 $preferences = array();
702 $preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
703 'permissioncallback' => function($user, $preferencename) {
704 global $USER;
705 $systemcontext = context_system::instance();
706 return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
707 ($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
708 });
709 $preferences['usemodchooser'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
710 'choices' => array(0, 1));
711 $preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
712 'choices' => array(0, 1));
713 $preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
714 'cleancallback' => function($value, $preferencename) {
715 if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
716 return null;
717 }
718 return $value;
719 });
720 $preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
721 'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
722 global $CFG, $USER;
723 return !empty($CFG->enablebadges) && $user->id == $USER->id;
724 });
725 $preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
726 'permissioncallback' => function($user, $preferencename) {
727 global $USER;
728 return $USER->id == $user->id && has_capability('moodle/blog:view', context_system::instance());
729 });
730
731 // Core components that may want to define their preferences.
732 // List of core components implementing callback is hardcoded here for performance reasons.
733 // TODO MDL-58184 cache list of core components implementing a function.
734 $corecomponents = ['core_message', 'core_calendar'];
735 foreach ($corecomponents as $component) {
736 if (($pluginpreferences = component_callback($component, 'user_preferences')) && is_array($pluginpreferences)) {
737 $preferences += $pluginpreferences;
738 }
739 }
740
741 // Plugins that may define their preferences.
742 if ($pluginsfunction = get_plugins_with_function('user_preferences')) {
743 foreach ($pluginsfunction as $plugintype => $plugins) {
744 foreach ($plugins as $function) {
745 if (($pluginpreferences = call_user_func($function)) && is_array($pluginpreferences)) {
746 $preferences += $pluginpreferences;
747 }
748 }
749 }
750 }
751
752 self::$preferencescache = $preferences;
753 }
754
755 /**
756 * Retrieves the preference definition
757 *
758 * @param string $preferencename
759 * @return array
760 */
761 protected static function get_preference_definition($preferencename) {
762 self::fill_preferences_cache();
763
764 foreach (self::$preferencescache as $key => $preference) {
765 if (empty($preference['isregex'])) {
766 if ($key === $preferencename) {
767 return $preference;
768 }
769 } else {
770 if (preg_match($key, $preferencename)) {
771 return $preference;
772 }
773 }
774 }
775
776 throw new coding_exception('Invalid preference requested.');
777 }
778
779 /**
780 * Default callback used for checking if current user is allowed to change permission of user $user
781 *
782 * @param stdClass $user
783 * @param string $preferencename
784 * @return bool
785 */
786 protected static function default_preference_permission_check($user, $preferencename) {
787 global $USER;
788 if (is_mnet_remote_user($user)) {
789 // Can't edit MNET user.
790 return false;
791 }
792
793 if ($user->id == $USER->id) {
794 // Editing own profile.
795 $systemcontext = context_system::instance();
796 return has_capability('moodle/user:editownprofile', $systemcontext);
797 } else {
798 // Teachers, parents, etc.
799 $personalcontext = context_user::instance($user->id);
800 if (!has_capability('moodle/user:editprofile', $personalcontext)) {
801 return false;
802 }
803 if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
804 // Only admins may edit other admins.
805 return false;
806 }
807 return true;
808 }
809 }
810
811 /**
812 * Can current user edit preference of this/another user
813 *
814 * @param string $preferencename
815 * @param stdClass $user
816 * @return bool
817 */
818 public static function can_edit_preference($preferencename, $user) {
819 if (!isloggedin() || isguestuser()) {
820 // Guests can not edit anything.
821 return false;
822 }
823
824 try {
825 $definition = self::get_preference_definition($preferencename);
826 } catch (coding_exception $e) {
827 return false;
828 }
829
830 if ($user->deleted || !context_user::instance($user->id, IGNORE_MISSING)) {
831 // User is deleted.
832 return false;
833 }
834
835 if (isset($definition['permissioncallback'])) {
836 $callback = $definition['permissioncallback'];
837 if (is_callable($callback)) {
838 return call_user_func_array($callback, [$user, $preferencename]);
839 } else {
840 throw new coding_exception('Permission callback for preference ' . s($preferencename) . ' is not callable');
841 return false;
842 }
843 } else {
844 return self::default_preference_permission_check($user, $preferencename);
845 }
846 }
847
848 /**
849 * Clean value of a user preference
850 *
851 * @param string $value the user preference value to be cleaned.
852 * @param string $preferencename the user preference name
853 * @return string the cleaned preference value
854 */
855 public static function clean_preference($value, $preferencename) {
856
857 $definition = self::get_preference_definition($preferencename);
858
859 if (isset($definition['type']) && $value !== null) {
860 $value = clean_param($value, $definition['type']);
861 }
862
863 if (isset($definition['cleancallback'])) {
864 $callback = $definition['cleancallback'];
865 if (is_callable($callback)) {
866 return $callback($value, $preferencename);
867 } else {
868 throw new coding_exception('Clean callback for preference ' . s($preferencename) . ' is not callable');
869 }
870 } else if ($value === null && (!isset($definition['null']) || $definition['null'] == NULL_ALLOWED)) {
871 return null;
872 } else if (isset($definition['choices'])) {
873 if (!in_array($value, $definition['choices'])) {
874 if (isset($definition['default'])) {
875 return $definition['default'];
876 } else {
877 $first = reset($definition['choices']);
878 return $first;
879 }
880 } else {
881 return $value;
882 }
883 } else {
884 if ($value === null) {
885 return isset($definition['default']) ? $definition['default'] : '';
886 }
887 return $value;
888 }
889 }
3bcf6b3c 890}