MDL-61028 core_search: Fix SELECT bug affecting Oracle
[moodle.git] / lib / classes / user.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * 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  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * User class to access user details.
29  *
30  * @todo       move api's from user/lib.php and deprecate 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  */
35 class core_user {
36     /**
37      * No reply user id.
38      */
39     const NOREPLY_USER = -10;
41     /**
42      * Support user id.
43      */
44     const SUPPORT_USER = -20;
46     /**
47      * Hide email address from everyone.
48      */
49     const MAILDISPLAY_HIDE = 0;
51     /**
52      * Display email address to everyone.
53      */
54     const MAILDISPLAY_EVERYONE = 1;
56     /**
57      * Display email address to course members only.
58      */
59     const MAILDISPLAY_COURSE_MEMBERS_ONLY = 2;
61     /**
62      * List of fields that can be synched/locked during authentication.
63      */
64     const AUTHSYNCFIELDS = [
65         'firstname',
66         'lastname',
67         'email',
68         'city',
69         'country',
70         'lang',
71         'description',
72         'url',
73         'idnumber',
74         'institution',
75         'department',
76         'phone1',
77         'phone2',
78         'address',
79         'firstnamephonetic',
80         'lastnamephonetic',
81         'middlename',
82         'alternatename'
83     ];
85     /** @var stdClass keep record of noreply user */
86     public static $noreplyuser = false;
88     /** @var stdClass keep record of support user */
89     public static $supportuser = false;
91     /** @var array store user fields properties cache. */
92     protected static $propertiescache = null;
94     /** @var array store user preferences cache. */
95     protected static $preferencescache = null;
97     /**
98      * Return user object from db or create noreply or support user,
99      * if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
100      * respectively. If userid is not found, then return false.
101      *
102      * @param int $userid user id
103      * @param string $fields A comma separated list of user fields to be returned, support and noreply user
104      *                       will not be filtered by this.
105      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
106      *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
107      *                        MUST_EXIST means throw an exception if no user record or multiple records found.
108      * @return stdClass|bool user record if found, else false.
109      * @throws dml_exception if user record not found and respective $strictness is set.
110      */
111     public static function get_user($userid, $fields = '*', $strictness = IGNORE_MISSING) {
112         global $DB;
114         // If noreply user then create fake record and return.
115         switch ($userid) {
116             case self::NOREPLY_USER:
117                 return self::get_noreply_user();
118                 break;
119             case self::SUPPORT_USER:
120                 return self::get_support_user();
121                 break;
122             default:
123                 return $DB->get_record('user', array('id' => $userid), $fields, $strictness);
124         }
125     }
127     /**
128      * Return user object from db based on their email.
129      *
130      * @param string $email The email 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_email($email, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
140         global $DB, $CFG;
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         }
148         return $DB->get_record('user', array('email' => $email, 'mnethostid' => $mnethostid), $fields, $strictness);
149     }
151     /**
152      * Return user object from db based on their username.
153      *
154      * @param string $username The username of the user searched.
155      * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
156      * @param int $mnethostid The id of the remote host.
157      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
158      *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
159      *                        MUST_EXIST means throw an exception if no user record or multiple records found.
160      * @return stdClass|bool user record if found, else false.
161      * @throws dml_exception if user record not found and respective $strictness is set.
162      */
163     public static function get_user_by_username($username, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
164         global $DB, $CFG;
166         // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
167         if (empty($mnethostid)) {
168             // If empty, we restrict to local users.
169             $mnethostid = $CFG->mnet_localhost_id;
170         }
172         return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness);
173     }
175     /**
176      * Searches for users by name, possibly within a specified context, with current user's access.
177      *
178      * Deciding which users to search is complicated because it relies on user permissions;
179      * ideally, we shouldn't show names if you aren't allowed to see their profile. The permissions
180      * for seeing profile are really complicated.
181      *
182      * Even if search is restricted to a course, it's possible that other people might have
183      * been able to contribute within the course (e.g. they were enrolled before and not now;
184      * or people with system-level roles) so if the user has permission we do want to include
185      * everyone. However, if there are multiple results then we prioritise the ones who are
186      * enrolled in the course.
187      *
188      * If you have moodle/user:viewdetails at system level, you can search everyone.
189      * Otherwise we check which courses you *do* have that permission and search everyone who is
190      * enrolled on those courses.
191      *
192      * Normally you can only search the user's name. If you have the moodle/site:viewuseridentity
193      * capability then we also let you search the fields which are listed as identity fields in
194      * the 'showuseridentity' config option. For example, this might include the user's ID number
195      * or email.
196      *
197      * The $max parameter controls the maximum number of users returned. If users are restricted
198      * from view for some reason, multiple runs of the main query might be made; the $querylimit
199      * parameter allows this to be restricted. Both parameters can be zero to remove limits.
200      *
201      * The returned user objects include id, username, all fields required for user pictures, and
202      * user identity fields.
203      *
204      * @param string $query Search query text
205      * @param \context_course|null $coursecontext Course context or null if system-wide
206      * @param int $max Max number of users to return, default 30 (zero = no limit)
207      * @param int $querylimit Max number of database queries, default 5 (zero = no limit)
208      * @return array Array of user objects with limited fields
209      */
210     public static function search($query, \context_course $coursecontext = null,
211             $max = 30, $querylimit = 5) {
212         global $CFG, $DB;
213         require_once($CFG->dirroot . '/user/lib.php');
215         // Allow limits to be turned off.
216         if (!$max) {
217             $max = PHP_INT_MAX;
218         }
219         if (!$querylimit) {
220             $querylimit = PHP_INT_MAX;
221         }
223         // Check permission to view profiles at each context.
224         $systemcontext = \context_system::instance();
225         $viewsystem = has_capability('moodle/user:viewdetails', $systemcontext);
226         if ($viewsystem) {
227             $userquery = 'SELECT id FROM {user}';
228             $userparams = [];
229         }
230         if (!$viewsystem) {
231             list($userquery, $userparams) = self::get_enrolled_sql_on_courses_with_capability(
232                     'moodle/user:viewdetails');
233             if (!$userquery) {
234                 // No permissions anywhere, return nothing.
235                 return [];
236             }
237         }
239         // Start building the WHERE clause based on name.
240         list ($where, $whereparams) = users_search_sql($query, 'u', false);
242         // We allow users to search with extra identity fields (as well as name) but only if they
243         // have the permission to display those identity fields.
244         $extrasql = '';
245         $extraparams = [];
247         if (empty($CFG->showuseridentity)) {
248             // Explode gives wrong result with empty string.
249             $extra = [];
250         } else {
251             $extra = explode(',', $CFG->showuseridentity);
252         }
254         // We need the username just to skip guests.
255         $extrafieldlist = $extra;
256         if (!in_array('username', $extra)) {
257             $extrafieldlist[] = 'username';
258         }
259         // The deleted flag will always be false because users_search_sql excludes deleted users,
260         // but it must be present or it causes PHP warnings in some functions below.
261         if (!in_array('deleted', $extra)) {
262             $extrafieldlist[] = 'deleted';
263         }
264         $selectfields = \user_picture::fields('u',
265                 array_merge(get_all_user_name_fields(), $extrafieldlist));
267         $index = 1;
268         foreach ($extra as $fieldname) {
269             if ($extrasql) {
270                 $extrasql .= ' OR ';
271             }
272             $extrasql .= $DB->sql_like('u.' . $fieldname, ':extra' . $index, false);
273             $extraparams['extra' . $index] = $query . '%';
274             $index++;
275         }
277         $identitysystem = has_capability('moodle/site:viewuseridentity', $systemcontext);
278         $usingshowidentity = false;
279         if ($identitysystem) {
280             // They have permission everywhere so just add the extra query to the normal query.
281             $where .= ' OR ' . $extrasql;
282             $whereparams = array_merge($whereparams, $extraparams);
283         } else {
284             // Get all courses where user can view full user identity.
285             list($sql, $params) = self::get_enrolled_sql_on_courses_with_capability(
286                     'moodle/site:viewuseridentity');
287             if ($sql) {
288                 // Join that with the user query to get an extra field indicating if we can.
289                 $userquery = "
290                         SELECT innerusers.id, COUNT(identityusers.id) AS showidentity
291                           FROM ($userquery) innerusers
292                      LEFT JOIN ($sql) identityusers ON identityusers.id = innerusers.id
293                       GROUP BY innerusers.id";
294                 $userparams = array_merge($userparams, $params);
295                 $usingshowidentity = true;
297                 // Query on the extra fields only in those places.
298                 $where .= ' OR (users.showidentity > 0 AND (' . $extrasql . '))';
299                 $whereparams = array_merge($whereparams, $extraparams);
300             }
301         }
303         // Default order is just name order. But if searching within a course then we show users
304         // within the course first.
305         list ($order, $orderparams) = users_order_by_sql('u', $query, $systemcontext);
306         if ($coursecontext) {
307             list ($sql, $params) = get_enrolled_sql($coursecontext);
308             $mainfield = 'innerusers2.id';
309             if ($usingshowidentity) {
310                 $mainfield .= ', innerusers2.showidentity';
311             }
312             $userquery = "
313                     SELECT $mainfield, COUNT(courseusers.id) AS incourse
314                       FROM ($userquery) innerusers2
315                  LEFT JOIN ($sql) courseusers ON courseusers.id = innerusers2.id
316                   GROUP BY $mainfield";
317             $userparams = array_merge($userparams, $params);
319             $order = 'incourse DESC, ' . $order;
320         }
322         // Get result (first 30 rows only) from database. Take a couple spare in case we have to
323         // drop some.
324         $result = [];
325         $got = 0;
326         $pos = 0;
327         $readcount = $max + 2;
328         for ($i = 0; $i < $querylimit; $i++) {
329             $rawresult = $DB->get_records_sql("
330                     SELECT $selectfields
331                       FROM ($userquery) users
332                       JOIN {user} u ON u.id = users.id
333                      WHERE $where
334                   ORDER BY $order", array_merge($userparams, $whereparams, $orderparams),
335                     $pos, $readcount);
336             foreach ($rawresult as $user) {
337                 // Skip guest.
338                 if ($user->username === 'guest') {
339                     continue;
340                 }
341                 // Check user can really view profile (there are per-user cases where this could
342                 // be different for some reason, this is the same check used by the profile view pages
343                 // to double-check that it is OK).
344                 if (!user_can_view_profile($user)) {
345                     continue;
346                 }
347                 $result[] = $user;
348                 $got++;
349                 if ($got >= $max) {
350                     break;
351                 }
352             }
354             if ($got >= $max) {
355                 // All necessary results obtained.
356                 break;
357             }
358             if (count($rawresult) < $readcount) {
359                 // No more results from database.
360                 break;
361             }
362             $pos += $readcount;
363         }
365         return $result;
366     }
368     /**
369      * Gets an SQL query that lists all enrolled user ids on any course where the current
370      * user has the specified capability. Helper function used for searching users.
371      *
372      * @param string $capability Required capability
373      * @return array Array containing SQL and params, or two nulls if there are no courses
374      */
375     protected static function get_enrolled_sql_on_courses_with_capability($capability) {
376         // Get all courses where user have the capability.
377         $courses = get_user_capability_course($capability, null, true,
378                 'ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance');
379         if (!$courses) {
380             return [null, null];
381         }
383         // Loop around all courses getting the SQL for enrolled users. Note: This query could
384         // probably be more efficient (without the union) if get_enrolled_sql had a way to
385         // pass an array of courseids, but it doesn't.
386         $unionsql = '';
387         $unionparams = [];
388         foreach ($courses as $course) {
389             // Get SQL to list user ids enrolled in this course.
390             \context_helper::preload_from_record($course);
391             list ($sql, $params) = get_enrolled_sql(\context_course::instance($course->id));
393             // Combine to a big union query.
394             if ($unionsql) {
395                 $unionsql .= ' UNION ';
396             }
397             $unionsql .= $sql;
398             $unionparams = array_merge($unionparams, $params);
399         }
401         return [$unionsql, $unionparams];
402     }
404     /**
405      * Helper function to return dummy noreply user record.
406      *
407      * @return stdClass
408      */
409     protected static function get_dummy_user_record() {
410         global $CFG;
412         $dummyuser = new stdClass();
413         $dummyuser->id = self::NOREPLY_USER;
414         $dummyuser->email = $CFG->noreplyaddress;
415         $dummyuser->firstname = get_string('noreplyname');
416         $dummyuser->username = 'noreply';
417         $dummyuser->lastname = '';
418         $dummyuser->confirmed = 1;
419         $dummyuser->suspended = 0;
420         $dummyuser->deleted = 0;
421         $dummyuser->picture = 0;
422         $dummyuser->auth = 'manual';
423         $dummyuser->firstnamephonetic = '';
424         $dummyuser->lastnamephonetic = '';
425         $dummyuser->middlename = '';
426         $dummyuser->alternatename = '';
427         $dummyuser->imagealt = '';
428         return $dummyuser;
429     }
431     /**
432      * Return noreply user record, this is currently used in messaging
433      * system only for sending messages from noreply email.
434      * It will return record of $CFG->noreplyuserid if set else return dummy
435      * user object with hard-coded $user->emailstop = 1 so noreply can be sent to user.
436      *
437      * @return stdClass user record.
438      */
439     public static function get_noreply_user() {
440         global $CFG;
442         if (!empty(self::$noreplyuser)) {
443             return self::$noreplyuser;
444         }
446         // If noreply user is set then use it, else create one.
447         if (!empty($CFG->noreplyuserid)) {
448             self::$noreplyuser = self::get_user($CFG->noreplyuserid);
449             self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
450             return self::$noreplyuser;
451         } else {
452             // Do not cache the dummy user record to avoid language internationalization issues.
453             $noreplyuser = self::get_dummy_user_record();
454             $noreplyuser->maildisplay = '1'; // Show to all.
455             $noreplyuser->emailstop = 1;
456             return $noreplyuser;
457         }
458     }
460     /**
461      * Return support user record, this is currently used in messaging
462      * system only for sending messages to support email.
463      * $CFG->supportuserid is set then returns user record
464      * $CFG->supportemail is set then return dummy record with $CFG->supportemail
465      * else return admin user record with hard-coded $user->emailstop = 0, so user
466      * gets support message.
467      *
468      * @return stdClass user record.
469      */
470     public static function get_support_user() {
471         global $CFG;
473         if (!empty(self::$supportuser)) {
474             return self::$supportuser;
475         }
477         // If custom support user is set then use it, else if supportemail is set then use it, else use noreply.
478         if (!empty($CFG->supportuserid)) {
479             self::$supportuser = self::get_user($CFG->supportuserid, '*', MUST_EXIST);
480         } else if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
481             // Try sending it to support email if support user is not set.
482             $supportuser = self::get_dummy_user_record();
483             $supportuser->id = self::SUPPORT_USER;
484             $supportuser->email = $CFG->supportemail;
485             if ($CFG->supportname) {
486                 $supportuser->firstname = $CFG->supportname;
487             }
488             $supportuser->username = 'support';
489             $supportuser->maildisplay = '1'; // Show to all.
490             // Unset emailstop to make sure support message is sent.
491             $supportuser->emailstop = 0;
492             return $supportuser;
493         }
495         // Send support msg to admin user if nothing is set above.
496         if (empty(self::$supportuser)) {
497             self::$supportuser = get_admin();
498         }
500         // Unset emailstop to make sure support message is sent.
501         self::$supportuser->emailstop = 0;
502         return self::$supportuser;
503     }
505     /**
506      * Reset self::$noreplyuser and self::$supportuser.
507      * This is only used by phpunit, and there is no other use case for this function.
508      * Please don't use it outside phpunit.
509      */
510     public static function reset_internal_users() {
511         if (PHPUNIT_TEST) {
512             self::$noreplyuser = false;
513             self::$supportuser = false;
514         } else {
515             debugging('reset_internal_users() should not be used outside phpunit.', DEBUG_DEVELOPER);
516         }
517     }
519     /**
520      * Return true is user id is greater than self::NOREPLY_USER and
521      * alternatively check db.
522      *
523      * @param int $userid user id.
524      * @param bool $checkdb if true userid will be checked in db. By default it's false, and
525      *                      userid is compared with NOREPLY_USER for performance.
526      * @return bool true is real user else false.
527      */
528     public static function is_real_user($userid, $checkdb = false) {
529         global $DB;
531         if ($userid < 0) {
532             return false;
533         }
534         if ($checkdb) {
535             return $DB->record_exists('user', array('id' => $userid));
536         } else {
537             return true;
538         }
539     }
541     /**
542      * Check if the given user is an active user in the site.
543      *
544      * @param  stdClass  $user         user object
545      * @param  boolean $checksuspended whether to check if the user has the account suspended
546      * @param  boolean $checknologin   whether to check if the user uses the nologin auth method
547      * @throws moodle_exception
548      * @since  Moodle 3.0
549      */
550     public static function require_active_user($user, $checksuspended = false, $checknologin = false) {
552         if (!self::is_real_user($user->id)) {
553             throw new moodle_exception('invaliduser', 'error');
554         }
556         if ($user->deleted) {
557             throw new moodle_exception('userdeleted');
558         }
560         if (empty($user->confirmed)) {
561             throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
562         }
564         if (isguestuser($user)) {
565             throw new moodle_exception('guestsarenotallowed', 'error');
566         }
568         if ($checksuspended and $user->suspended) {
569             throw new moodle_exception('suspended', 'auth');
570         }
572         if ($checknologin and $user->auth == 'nologin') {
573             throw new moodle_exception('suspended', 'auth');
574         }
575     }
577     /**
578      * Updates the provided users profile picture based upon the expected fields returned from the edit or edit_advanced forms.
579      *
580      * @param stdClass $usernew An object that contains some information about the user being updated
581      * @param array $filemanageroptions
582      * @return bool True if the user was updated, false if it stayed the same.
583      */
584     public static function update_picture(stdClass $usernew, $filemanageroptions = array()) {
585         global $CFG, $DB;
586         require_once("$CFG->libdir/gdlib.php");
588         $context = context_user::instance($usernew->id, MUST_EXIST);
589         $user = core_user::get_user($usernew->id, 'id, picture', MUST_EXIST);
591         $newpicture = $user->picture;
592         // Get file_storage to process files.
593         $fs = get_file_storage();
594         if (!empty($usernew->deletepicture)) {
595             // The user has chosen to delete the selected users picture.
596             $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
597             $newpicture = 0;
599         } else {
600             // Save newly uploaded file, this will avoid context mismatch for newly created users.
601             file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
602             if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
603                 // Get file which was uploaded in draft area.
604                 foreach ($iconfiles as $file) {
605                     if (!$file->is_directory()) {
606                         break;
607                     }
608                 }
609                 // Copy file to temporary location and the send it for processing icon.
610                 if ($iconfile = $file->copy_content_to_temp()) {
611                     // There is a new image that has been uploaded.
612                     // Process the new image and set the user to make use of it.
613                     // NOTE: Uploaded images always take over Gravatar.
614                     $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
615                     // Delete temporary file.
616                     @unlink($iconfile);
617                     // Remove uploaded file.
618                     $fs->delete_area_files($context->id, 'user', 'newicon');
619                 } else {
620                     // Something went wrong while creating temp file.
621                     // Remove uploaded file.
622                     $fs->delete_area_files($context->id, 'user', 'newicon');
623                     return false;
624                 }
625             }
626         }
628         if ($newpicture != $user->picture) {
629             $DB->set_field('user', 'picture', $newpicture, array('id' => $user->id));
630             return true;
631         } else {
632             return false;
633         }
634     }
638     /**
639      * Definition of user profile fields and the expected parameter type for data validation.
640      *
641      * array(
642      *     'property_name' => array(       // The user property to be checked. Should match the field on the user table.
643      *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
644      *          'type' => PARAM_TYPE,      // Expected parameter type of the user field.
645      *          'choices' => array(1, 2..) // An array of accepted values of the user field.
646      *          'default' => $CFG->setting // An default value for the field.
647      *     )
648      * )
649      *
650      * The fields choices and default are optional.
651      *
652      * @return void
653      */
654     protected static function fill_properties_cache() {
655         global $CFG;
656         if (self::$propertiescache !== null) {
657             return;
658         }
660         // Array of user fields properties and expected parameters.
661         // Every new field on the user table should be added here otherwise it won't be validated.
662         $fields = array();
663         $fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
664         $fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
665         $fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
666         $fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
667         $fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
668         $fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
669         $fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
670         $fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
671         $fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
672         $fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
673         $fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
674         $fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
675         $fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
676         $fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
677         $fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
678         $fields['icq'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
679         $fields['skype'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
680         $fields['aim'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
681         $fields['yahoo'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
682         $fields['msn'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
683         $fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
684         $fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
685         $fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
686         $fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
687         $fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
688         $fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
689         $fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
690                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
691         $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->lang,
692                 'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
693         $fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
694                 'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
695         $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
696                 'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
697         $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
698                 'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
699         $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
700         $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
701         $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
702         $fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
703         $fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
704         $fields['secret'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
705         $fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
706         $fields['url'] = array('type' => PARAM_URL, 'null' => NULL_NOT_ALLOWED);
707         $fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
708         $fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
709         $fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
710                 'default' => $CFG->defaultpreference_mailformat);
711         $fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
712                 'default' => $CFG->defaultpreference_maildigest);
713         $fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
714                 'default' => $CFG->defaultpreference_maildisplay);
715         $fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
716                 'default' => $CFG->defaultpreference_autosubscribe);
717         $fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
718                 'default' => $CFG->defaultpreference_trackforums);
719         $fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
720         $fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
721         $fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
722         $fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
723         $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
724         $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
725         $fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
726         $fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
728         self::$propertiescache = $fields;
729     }
731     /**
732      * Get properties of a user field.
733      *
734      * @param string $property property name to be retrieved.
735      * @throws coding_exception if the requested property name is invalid.
736      * @return array the property definition.
737      */
738     public static function get_property_definition($property) {
740         self::fill_properties_cache();
742         if (!array_key_exists($property, self::$propertiescache)) {
743             throw new coding_exception('Invalid property requested.');
744         }
746         return self::$propertiescache[$property];
747     }
749     /**
750      * Validate user data.
751      *
752      * This method just validates each user field and return an array of errors. It doesn't clean the data,
753      * the methods clean() and clean_field() should be used for this purpose.
754      *
755      * @param stdClass|array $data user data object or array to be validated.
756      * @return array|true $errors array of errors found on the user object, true if the validation passed.
757      */
758     public static function validate($data) {
759         // Get all user profile fields definition.
760         self::fill_properties_cache();
762         foreach ($data as $property => $value) {
763             try {
764                 if (isset(self::$propertiescache[$property])) {
765                     validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
766                 }
767                 // Check that the value is part of a list of allowed values.
768                 if (!empty(self::$propertiescache[$property]['choices']) &&
769                         !isset(self::$propertiescache[$property]['choices'][$value])) {
770                     throw new invalid_parameter_exception($value);
771                 }
772             } catch (invalid_parameter_exception $e) {
773                 $errors[$property] = $e->getMessage();
774             }
775         }
777         return empty($errors) ? true : $errors;
778     }
780     /**
781      * Clean the properties cache.
782      *
783      * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
784      * Intended for use only for testing, phpunit calls this before every test.
785      */
786     public static function reset_caches() {
787         self::$propertiescache = null;
788     }
790     /**
791      * Clean the user data.
792      *
793      * @param stdClass|array $user the user data to be validated against properties definition.
794      * @return stdClass $user the cleaned user data.
795      */
796     public static function clean_data($user) {
797         if (empty($user)) {
798             return $user;
799         }
801         foreach ($user as $field => $value) {
802             // Get the property parameter type and do the cleaning.
803             try {
804                 $user->$field = core_user::clean_field($value, $field);
805             } catch (coding_exception $e) {
806                 debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
807             }
808         }
810         return $user;
811     }
813     /**
814      * Clean a specific user field.
815      *
816      * @param string $data the user field data to be cleaned.
817      * @param string $field the user field name on the property definition cache.
818      * @return string the cleaned user data.
819      */
820     public static function clean_field($data, $field) {
821         if (empty($data) || empty($field)) {
822             return $data;
823         }
825         try {
826             $type = core_user::get_property_type($field);
828             if (isset(self::$propertiescache[$field]['choices'])) {
829                 if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
830                     if (isset(self::$propertiescache[$field]['default'])) {
831                         $data = self::$propertiescache[$field]['default'];
832                     } else {
833                         $data = '';
834                     }
835                 } else {
836                     return $data;
837                 }
838             } else {
839                 $data = clean_param($data, $type);
840             }
841         } catch (coding_exception $e) {
842             debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
843         }
845         return $data;
846     }
848     /**
849      * Get the parameter type of the property.
850      *
851      * @param string $property property name to be retrieved.
852      * @throws coding_exception if the requested property name is invalid.
853      * @return int the property parameter type.
854      */
855     public static function get_property_type($property) {
857         self::fill_properties_cache();
859         if (!array_key_exists($property, self::$propertiescache)) {
860             throw new coding_exception('Invalid property requested: ' . $property);
861         }
863         return self::$propertiescache[$property]['type'];
864     }
866     /**
867      * Discover if the property is NULL_ALLOWED or NULL_NOT_ALLOWED.
868      *
869      * @param string $property property name to be retrieved.
870      * @throws coding_exception if the requested property name is invalid.
871      * @return bool true if the property is NULL_ALLOWED, false otherwise.
872      */
873     public static function get_property_null($property) {
875         self::fill_properties_cache();
877         if (!array_key_exists($property, self::$propertiescache)) {
878             throw new coding_exception('Invalid property requested: ' . $property);
879         }
881         return self::$propertiescache[$property]['null'];
882     }
884     /**
885      * Get the choices of the property.
886      *
887      * This is a helper method to validate a value against a list of acceptable choices.
888      * For instance: country, language, themes and etc.
889      *
890      * @param string $property property name to be retrieved.
891      * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
892      * @return array the property parameter type.
893      */
894     public static function get_property_choices($property) {
896         self::fill_properties_cache();
898         if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
899                 self::$propertiescache[$property])) {
901             throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
902         }
904         return self::$propertiescache[$property]['choices'];
905     }
907     /**
908      * Get the property default.
909      *
910      * This method gets the default value of a field (if exists).
911      *
912      * @param string $property property name to be retrieved.
913      * @throws coding_exception if the requested property name is invalid or if it does not has a default value.
914      * @return string the property default value.
915      */
916     public static function get_property_default($property) {
918         self::fill_properties_cache();
920         if (!array_key_exists($property, self::$propertiescache) || !isset(self::$propertiescache[$property]['default'])) {
921             throw new coding_exception('Invalid property requested, or the property does not has a default value.');
922         }
924         return self::$propertiescache[$property]['default'];
925     }
927     /**
928      * Definition of updateable user preferences and rules for data and access validation.
929      *
930      * array(
931      *     'preferencename' => array(      // Either exact preference name or a regular expression.
932      *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
933      *          'type' => PARAM_TYPE,      // Expected parameter type of the user field - mandatory
934      *          'choices' => array(1, 2..) // An array of accepted values of the user field - optional
935      *          'default' => $CFG->setting // An default value for the field - optional
936      *          'isregex' => false/true    // Whether the name of the preference is a regular expression (default false).
937      *          'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
938      *                                     // is allowed to modify this preference for given user.
939      *                                     // If not specified core_user::default_preference_permission_check() will be assumed.
940      *          'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
941      *                                     // accepts arguments ($value, $preferencename)
942      *     )
943      * )
944      *
945      * @return void
946      */
947     protected static function fill_preferences_cache() {
948         if (self::$preferencescache !== null) {
949             return;
950         }
952         // Array of user preferences and expected types/values.
953         // Every preference that can be updated directly by user should be added here.
954         $preferences = array();
955         $preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
956             'permissioncallback' => function($user, $preferencename) {
957                 global $USER;
958                 $systemcontext = context_system::instance();
959                 return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
960                         ($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
961             });
962         $preferences['usemodchooser'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
963             'choices' => array(0, 1));
964         $preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
965             'choices' => array(0, 1));
966         $preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
967             'cleancallback' => function($value, $preferencename) {
968                 if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
969                     return null;
970                 }
971                 return $value;
972             });
973         $preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
974             'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
975                 global $CFG, $USER;
976                 return !empty($CFG->enablebadges) && $user->id == $USER->id;
977             });
978         $preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
979             'permissioncallback' => function($user, $preferencename) {
980                 global $USER;
981                 return $USER->id == $user->id && has_capability('moodle/blog:view', context_system::instance());
982             });
984         // Core components that may want to define their preferences.
985         // List of core components implementing callback is hardcoded here for performance reasons.
986         // TODO MDL-58184 cache list of core components implementing a function.
987         $corecomponents = ['core_message', 'core_calendar'];
988         foreach ($corecomponents as $component) {
989             if (($pluginpreferences = component_callback($component, 'user_preferences')) && is_array($pluginpreferences)) {
990                 $preferences += $pluginpreferences;
991             }
992         }
994         // Plugins that may define their preferences.
995         if ($pluginsfunction = get_plugins_with_function('user_preferences')) {
996             foreach ($pluginsfunction as $plugintype => $plugins) {
997                 foreach ($plugins as $function) {
998                     if (($pluginpreferences = call_user_func($function)) && is_array($pluginpreferences)) {
999                         $preferences += $pluginpreferences;
1000                     }
1001                 }
1002             }
1003         }
1005         self::$preferencescache = $preferences;
1006     }
1008     /**
1009      * Retrieves the preference definition
1010      *
1011      * @param string $preferencename
1012      * @return array
1013      */
1014     protected static function get_preference_definition($preferencename) {
1015         self::fill_preferences_cache();
1017         foreach (self::$preferencescache as $key => $preference) {
1018             if (empty($preference['isregex'])) {
1019                 if ($key === $preferencename) {
1020                     return $preference;
1021                 }
1022             } else {
1023                 if (preg_match($key, $preferencename)) {
1024                     return $preference;
1025                 }
1026             }
1027         }
1029         throw new coding_exception('Invalid preference requested.');
1030     }
1032     /**
1033      * Default callback used for checking if current user is allowed to change permission of user $user
1034      *
1035      * @param stdClass $user
1036      * @param string $preferencename
1037      * @return bool
1038      */
1039     protected static function default_preference_permission_check($user, $preferencename) {
1040         global $USER;
1041         if (is_mnet_remote_user($user)) {
1042             // Can't edit MNET user.
1043             return false;
1044         }
1046         if ($user->id == $USER->id) {
1047             // Editing own profile.
1048             $systemcontext = context_system::instance();
1049             return has_capability('moodle/user:editownprofile', $systemcontext);
1050         } else  {
1051             // Teachers, parents, etc.
1052             $personalcontext = context_user::instance($user->id);
1053             if (!has_capability('moodle/user:editprofile', $personalcontext)) {
1054                 return false;
1055             }
1056             if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
1057                 // Only admins may edit other admins.
1058                 return false;
1059             }
1060             return true;
1061         }
1062     }
1064     /**
1065      * Can current user edit preference of this/another user
1066      *
1067      * @param string $preferencename
1068      * @param stdClass $user
1069      * @return bool
1070      */
1071     public static function can_edit_preference($preferencename, $user) {
1072         if (!isloggedin() || isguestuser()) {
1073             // Guests can not edit anything.
1074             return false;
1075         }
1077         try {
1078             $definition = self::get_preference_definition($preferencename);
1079         } catch (coding_exception $e) {
1080             return false;
1081         }
1083         if ($user->deleted || !context_user::instance($user->id, IGNORE_MISSING)) {
1084             // User is deleted.
1085             return false;
1086         }
1088         if (isset($definition['permissioncallback'])) {
1089             $callback = $definition['permissioncallback'];
1090             if (is_callable($callback)) {
1091                 return call_user_func_array($callback, [$user, $preferencename]);
1092             } else {
1093                 throw new coding_exception('Permission callback for preference ' . s($preferencename) . ' is not callable');
1094                 return false;
1095             }
1096         } else {
1097             return self::default_preference_permission_check($user, $preferencename);
1098         }
1099     }
1101     /**
1102      * Clean value of a user preference
1103      *
1104      * @param string $value the user preference value to be cleaned.
1105      * @param string $preferencename the user preference name
1106      * @return string the cleaned preference value
1107      */
1108     public static function clean_preference($value, $preferencename) {
1110         $definition = self::get_preference_definition($preferencename);
1112         if (isset($definition['type']) && $value !== null) {
1113             $value = clean_param($value, $definition['type']);
1114         }
1116         if (isset($definition['cleancallback'])) {
1117             $callback = $definition['cleancallback'];
1118             if (is_callable($callback)) {
1119                 return $callback($value, $preferencename);
1120             } else {
1121                 throw new coding_exception('Clean callback for preference ' . s($preferencename) . ' is not callable');
1122             }
1123         } else if ($value === null && (!isset($definition['null']) || $definition['null'] == NULL_ALLOWED)) {
1124             return null;
1125         } else if (isset($definition['choices'])) {
1126             if (!in_array($value, $definition['choices'])) {
1127                 if (isset($definition['default'])) {
1128                     return $definition['default'];
1129                 } else {
1130                     $first = reset($definition['choices']);
1131                     return $first;
1132                 }
1133             } else {
1134                 return $value;
1135             }
1136         } else {
1137             if ($value === null) {
1138                 return isset($definition['default']) ? $definition['default'] : '';
1139             }
1140             return $value;
1141         }
1142     }