Merge branch 'MDL-81457-main' of https://github.com/andrewnicols/moodle
[moodle.git] / user / classes / fields.php
blobc4154d833081660376d33803df56c3655ced5934
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.
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 namespace core_user;
19 use core_text;
21 /**
22 * Class for retrieving information about user fields that are needed for displaying user identity.
24 * @package core_user
26 class fields {
27 /** @var string Prefix used to identify custom profile fields */
28 const PROFILE_FIELD_PREFIX = 'profile_field_';
29 /** @var string Regular expression used to match a field name against the prefix */
30 const PROFILE_FIELD_REGEX = '~^' . self::PROFILE_FIELD_PREFIX . '(.*)$~';
32 /** @var int All fields required to display user's identity, based on server configuration */
33 const PURPOSE_IDENTITY = 0;
34 /** @var int All fields required to display a user picture */
35 const PURPOSE_USERPIC = 1;
36 /** @var int All fields required for somebody's name */
37 const PURPOSE_NAME = 2;
38 /** @var int Field required by custom include list */
39 const CUSTOM_INCLUDE = 3;
41 /** @var \context|null Context in use */
42 protected $context;
44 /** @var bool True to allow custom user fields */
45 protected $allowcustom;
47 /** @var bool[] Array of purposes (from PURPOSE_xx to true/false) */
48 protected $purposes;
50 /** @var string[] List of extra fields to include */
51 protected $include;
53 /** @var string[] List of fields to exclude */
54 protected $exclude;
56 /** @var int Unique identifier for different queries generated in same request */
57 protected static $uniqueidentifier = 1;
59 /** @var array|null Associative array from field => array of purposes it was used for => true */
60 protected $fields = null;
62 /**
63 * Protected constructor - use one of the for_xx methods to create an object.
65 * @param int $purpose Initial purpose for object or -1 for none
67 protected function __construct(int $purpose = -1) {
68 $this->purposes = [
69 self::PURPOSE_IDENTITY => false,
70 self::PURPOSE_USERPIC => false,
71 self::PURPOSE_NAME => false,
73 if ($purpose != -1) {
74 $this->purposes[$purpose] = true;
76 $this->include = [];
77 $this->exclude = [];
78 $this->context = null;
79 $this->allowcustom = true;
82 /**
83 * Constructs an empty user fields object to get arbitrary user fields.
85 * You can add fields to retrieve with the including() function.
87 * @return fields User fields object ready for use
89 public static function empty(): fields {
90 return new fields();
93 /**
94 * Constructs a user fields object to get identity information for display.
96 * The function does all the required capability checks to see if the current user is allowed
97 * to see them in the specified context. You can pass context null to get all the fields without
98 * checking permissions.
100 * If the code can only handle fields in the main user table, and not custom profile fields,
101 * then set $allowcustom to false.
103 * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
104 * functions to control the required fields in more detail. For example:
106 * $fields = fields::for_identity($context)->with_userpic()->excluding('email');
108 * @param \context|null $context Context; if supplied, includes only fields the current user should see
109 * @param bool $allowcustom If true, custom profile fields may be included
110 * @return fields User fields object ready for use
112 public static function for_identity(?\context $context, bool $allowcustom = true): fields {
113 $fields = new fields(self::PURPOSE_IDENTITY);
114 $fields->context = $context;
115 $fields->allowcustom = $allowcustom;
116 return $fields;
120 * Constructs a user fields object to get information required for displaying a user picture.
122 * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
123 * functions to control the required fields in more detail. For example:
125 * $fields = fields::for_userpic()->with_name()->excluding('email');
127 * @return fields User fields object ready for use
129 public static function for_userpic(): fields {
130 return new fields(self::PURPOSE_USERPIC);
134 * Constructs a user fields object to get information required for displaying a user full name.
136 * Note: After constructing the object you can use the ->with_xx, ->including, and ->excluding
137 * functions to control the required fields in more detail. For example:
139 * $fields = fields::for_name()->with_userpic()->excluding('email');
141 * @return fields User fields object ready for use
143 public static function for_name(): fields {
144 return new fields(self::PURPOSE_NAME);
148 * On an existing fields object, adds the fields required for displaying user pictures.
150 * @return $this Same object for chaining function calls
152 public function with_userpic(): fields {
153 $this->purposes[self::PURPOSE_USERPIC] = true;
154 return $this;
158 * On an existing fields object, adds the fields required for displaying user full names.
160 * @return $this Same object for chaining function calls
162 public function with_name(): fields {
163 $this->purposes[self::PURPOSE_NAME] = true;
164 return $this;
168 * On an existing fields object, adds the fields required for displaying user identity.
170 * The function does all the required capability checks to see if the current user is allowed
171 * to see them in the specified context. You can pass context null to get all the fields without
172 * checking permissions.
174 * If the code can only handle fields in the main user table, and not custom profile fields,
175 * then set $allowcustom to false.
177 * @param \context|null Context; if supplied, includes only fields the current user should see
178 * @param bool $allowcustom If true, custom profile fields may be included
179 * @return $this Same object for chaining function calls
181 public function with_identity(?\context $context, bool $allowcustom = true): fields {
182 $this->context = $context;
183 $this->allowcustom = $allowcustom;
184 $this->purposes[self::PURPOSE_IDENTITY] = true;
185 return $this;
189 * On an existing fields object, adds extra fields to be retrieved. You can specify either
190 * fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'.
192 * @param string ...$include One or more fields to add
193 * @return $this Same object for chaining function calls
195 public function including(string ...$include): fields {
196 $this->include = array_merge($this->include, $include);
197 return $this;
201 * On an existing fields object, excludes fields from retrieval. You can specify either
202 * fields from the user table e.g. 'email', or profile fields e.g. 'profile_field_height'.
204 * This is useful when constructing queries where your query already explicitly references
205 * certain fields, so you don't want to retrieve them twice.
207 * @param string ...$exclude One or more fields to exclude
208 * @return $this Same object for chaining function calls
210 public function excluding(...$exclude): fields {
211 $this->exclude = array_merge($this->exclude, $exclude);
212 return $this;
216 * Gets an array of all fields that are required for the specified purposes, also taking
217 * into account the $includes and $excludes settings.
219 * The results may include basic field names (columns from the 'user' database table) and,
220 * unless turned off, custom profile field names in the format 'profile_field_myfield'.
222 * You should not rely on the order of fields, with one exception: if there is an id field
223 * it will be returned first. This is in case it is used with get_records calls.
225 * The $limitpurposes parameter is useful if you want to get a different set of fields than the
226 * purposes in the constructor. For example, if you want to get SQL for identity + user picture
227 * fields, but you then want to only get the identity fields as a list. (You can only specify
228 * purposes that were also passed to the constructor i.e. it can only be used to restrict the
229 * list, not add to it.)
231 * @param array $limitpurposes If specified, gets fields only for these purposes
232 * @return string[] Array of required fields
233 * @throws \coding_exception If any unknown purpose is listed
235 public function get_required_fields(array $limitpurposes = []): array {
236 // The first time this is called, actually work out the list. There is no way to 'un-cache'
237 // it, but these objects are designed to be short-lived so it doesn't need one.
238 if ($this->fields === null) {
239 // Add all the fields as array keys so that there are no duplicates.
240 $this->fields = [];
241 if ($this->purposes[self::PURPOSE_IDENTITY]) {
242 foreach (self::get_identity_fields($this->context, $this->allowcustom) as $field) {
243 $this->fields[$field] = [self::PURPOSE_IDENTITY => true];
246 if ($this->purposes[self::PURPOSE_USERPIC]) {
247 foreach (self::get_picture_fields() as $field) {
248 if (!array_key_exists($field, $this->fields)) {
249 $this->fields[$field] = [];
251 $this->fields[$field][self::PURPOSE_USERPIC] = true;
254 if ($this->purposes[self::PURPOSE_NAME]) {
255 foreach (self::get_name_fields() as $field) {
256 if (!array_key_exists($field, $this->fields)) {
257 $this->fields[$field] = [];
259 $this->fields[$field][self::PURPOSE_NAME] = true;
262 foreach ($this->include as $field) {
263 if ($this->allowcustom || !preg_match(self::PROFILE_FIELD_REGEX, $field)) {
264 if (!array_key_exists($field, $this->fields)) {
265 $this->fields[$field] = [];
267 $this->fields[$field][self::CUSTOM_INCLUDE] = true;
270 foreach ($this->exclude as $field) {
271 unset($this->fields[$field]);
274 // If the id field is included, make sure it's first in the list.
275 if (array_key_exists('id', $this->fields)) {
276 $newfields = ['id' => $this->fields['id']];
277 foreach ($this->fields as $field => $purposes) {
278 if ($field !== 'id') {
279 $newfields[$field] = $purposes;
282 $this->fields = $newfields;
286 if ($limitpurposes) {
287 // Check the value was legitimate.
288 foreach ($limitpurposes as $purpose) {
289 if ($purpose != self::CUSTOM_INCLUDE && empty($this->purposes[$purpose])) {
290 throw new \coding_exception('$limitpurposes can only include purposes defined in object');
294 // Filter the fields to include only those matching the purposes.
295 $result = [];
296 foreach ($this->fields as $key => $purposes) {
297 foreach ($limitpurposes as $purpose) {
298 if (array_key_exists($purpose, $purposes)) {
299 $result[] = $key;
300 break;
304 return $result;
305 } else {
306 return array_keys($this->fields);
311 * Gets fields required for user pictures.
313 * The results include only basic field names (columns from the 'user' database table).
315 * @return string[] All fields required for user pictures
317 public static function get_picture_fields(): array {
318 return ['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic',
319 'middlename', 'alternatename', 'imagealt', 'email'];
323 * Gets fields required for user names.
325 * The results include only basic field names (columns from the 'user' database table).
327 * Fields are usually returned in a specific order, which the fullname() function depends on.
328 * If you specify 'true' to the $strangeorder flag, then the firstname and lastname fields
329 * are moved to the front; this is useful in a few places in existing code. New code should
330 * avoid requiring a particular order.
332 * @param bool $differentorder In a few places, a different order of fields is required
333 * @return string[] All fields used to display user names
335 public static function get_name_fields(bool $differentorder = false): array {
336 $fields = ['firstnamephonetic', 'lastnamephonetic', 'middlename', 'alternatename',
337 'firstname', 'lastname'];
338 if ($differentorder) {
339 return array_merge(array_slice($fields, -2), array_slice($fields, 0, -2));
340 } else {
341 return $fields;
346 * Gets all fields required for user identity. These fields should be included in tables
347 * showing lists of users (in addition to the user's name which is included as standard).
349 * The results include basic field names (columns from the 'user' database table) and, unless
350 * turned off, custom profile field names in the format 'profile_field_myfield', note these
351 * fields will always be returned lower cased to match how they are returned by the DML library.
353 * This function does all the required capability checks to see if the current user is allowed
354 * to see them in the specified context. You can pass context null to get all the fields
355 * without checking permissions.
357 * @param \context|null $context Context; if not supplied, all fields will be included without checks
358 * @param bool $allowcustom If true, custom profile fields will be included
359 * @return string[] Array of required fields
360 * @throws \coding_exception
362 public static function get_identity_fields(?\context $context, bool $allowcustom = true): array {
363 global $CFG;
365 // Only users with permission get the extra fields.
366 if ($context && !has_capability('moodle/site:viewuseridentity', $context)) {
367 return [];
370 // Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
371 $extra = array_filter(explode(',', $CFG->showuseridentity));
373 // If there are any custom fields, remove them if necessary (either if allowcustom is false,
374 // or if the user doesn't have access to see them).
375 foreach ($extra as $key => $field) {
376 if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
377 $allowed = false;
378 if ($allowcustom) {
379 require_once($CFG->dirroot . '/user/profile/lib.php');
381 // Ensure the field exists (it may have been deleted since user identity was configured).
382 $field = profile_get_custom_field_data_by_shortname($matches[1], false);
383 if ($field !== null) {
384 $fieldinstance = profile_get_user_field($field->datatype, $field->id, 0, $field);
385 $allowed = $fieldinstance->is_visible($context);
388 if (!$allowed) {
389 unset($extra[$key]);
394 // For standard user fields, access is controlled by the hiddenuserfields option and
395 // some different capabilities. Check and remove these if the user can't access them.
396 $hiddenfields = array_filter(explode(',', $CFG->hiddenuserfields));
397 $hiddenidentifiers = array_intersect($extra, $hiddenfields);
399 if ($hiddenidentifiers) {
400 if (!$context) {
401 $canviewhiddenuserfields = true;
402 } else if ($context->get_course_context(false)) {
403 // We are somewhere inside a course.
404 $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
405 } else {
406 // We are not inside a course.
407 $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
410 if (!$canviewhiddenuserfields) {
411 // Remove hidden identifiers from the list.
412 $extra = array_diff($extra, $hiddenidentifiers);
416 // Re-index the entries and return.
417 $extra = array_values($extra);
418 return array_map([core_text::class, 'strtolower'], $extra);
422 * Gets SQL that can be used in a query to get the necessary fields.
424 * The result of this function is an object with fields 'selects', 'joins', 'params', and
425 * 'mappings'.
427 * If not empty, the list of selects will begin with a comma and the list of joins will begin
428 * and end with a space. You can include the result in your existing query like this:
430 * SELECT (your existing fields)
431 * $selects
432 * FROM {user} u
433 * JOIN (your existing joins)
434 * $joins
436 * When there are no custom fields then the 'joins' result will always be an empty string, and
437 * 'params' will be an empty array.
439 * The $fieldmappings value is often not needed. It is an associative array from each field
440 * name to an SQL expression for the value of that field, e.g.:
441 * 'profile_field_frog' => 'uf1d_3.data'
442 * 'city' => 'u.city'
443 * This is helpful if you want to use the profile fields in a WHERE clause, becuase you can't
444 * refer to the aliases used in the SELECT list there.
446 * The leading comma is included because this makes it work in the pattern above even if there
447 * are no fields from the get_sql() data (which can happen if doing identity fields and none
448 * are selected). If you want the result without a leading comma, set $leadingcomma to false.
450 * If the 'id' field is included then it will always be first in the list. Otherwise, you
451 * should not rely on the field order.
453 * For identity fields, the function does all the required capability checks to see if the
454 * current user is allowed to see them in the specified context. You can pass context null
455 * to get all the fields without checking permissions.
457 * If your code for any reason cannot cope with custom fields then you can turn them off.
459 * You can have either named or ? params. If you use named params, they are of the form
460 * uf1s_2; the first number increments in each call using a static variable in this class and
461 * the second number refers to the field being queried. A similar pattern is used to make
462 * join aliases unique.
464 * If your query refers to the user table by an alias e.g. 'u' then specify this in the $alias
465 * parameter; otherwise it will use {user} (if there are any joins for custom profile fields)
466 * or simply refer to the field by name only (if there aren't).
468 * If you need to use a prefix on the field names (for example in case they might coincide with
469 * existing result columns from your query, or if you want a convenient way to split out all
470 * the user data into a separate object) then you can specify one here. For example, if you
471 * include name fields and the prefix is 'u_' then the results will include 'u_firstname'.
473 * If you don't want to prefix all the field names but only change the id field name, use
474 * the $renameid parameter. (When you use this parameter, it takes precedence over any prefix;
475 * the id field will not be prefixed, while all others will.)
477 * @param string $alias Optional (but recommended) alias for user table in query, e.g. 'u'
478 * @param bool $namedparams If true, uses named :parameters instead of indexed ? parameters
479 * @param string $prefix Optional prefix for all field names in result, e.g. 'u_'
480 * @param string $renameid Renames the 'id' field if specified, e.g. 'userid'
481 * @param bool $leadingcomma If true the 'selects' list will start with a comma
482 * @return \stdClass Object with necessary SQL components
484 public function get_sql(string $alias = '', bool $namedparams = false, string $prefix = '',
485 string $renameid = '', bool $leadingcomma = true): \stdClass {
486 global $DB;
488 $fields = $this->get_required_fields();
490 $selects = '';
491 $joins = '';
492 $params = [];
493 $mappings = [];
495 $unique = self::$uniqueidentifier++;
496 $fieldcount = 0;
498 if ($alias) {
499 $usertable = $alias . '.';
500 } else {
501 // If there is no alias, we still need to use {user} to identify the table when there
502 // are joins with other tables. When there are no customfields then there are no joins
503 // so we can refer to the fields by name alone.
504 $gotcustomfields = false;
505 foreach ($fields as $field) {
506 if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
507 $gotcustomfields = true;
508 break;
511 if ($gotcustomfields) {
512 $usertable = '{user}.';
513 } else {
514 $usertable = '';
518 foreach ($fields as $field) {
519 if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
520 // Custom profile field.
521 $shortname = $matches[1];
523 $fieldcount++;
525 $fieldalias = 'uf' . $unique . 'f_' . $fieldcount;
526 $dataalias = 'uf' . $unique . 'd_' . $fieldcount;
527 if ($namedparams) {
528 $withoutcolon = 'uf' . $unique . 's' . $fieldcount;
529 $placeholder = ':' . $withoutcolon;
530 $params[$withoutcolon] = $shortname;
531 } else {
532 $placeholder = '?';
533 $params[] = $shortname;
535 $joins .= " JOIN {user_info_field} $fieldalias ON " .
536 $DB->sql_equal($fieldalias . '.shortname', $placeholder, false) . "
537 LEFT JOIN {user_info_data} $dataalias ON $dataalias.fieldid = $fieldalias.id
538 AND $dataalias.userid = {$usertable}id";
539 // For Oracle we need to convert the field into a usable format.
540 $fieldsql = $DB->sql_compare_text($dataalias . '.data', 255);
541 $selects .= ", $fieldsql AS $prefix$field";
542 $mappings[$field] = $fieldsql;
543 } else {
544 // Standard user table field.
545 $selects .= ", $usertable$field";
546 if ($field === 'id' && $renameid && $renameid !== 'id') {
547 $selects .= " AS $renameid";
548 } else if ($prefix) {
549 $selects .= " AS $prefix$field";
551 $mappings[$field] = "$usertable$field";
555 // Add a space to the end of the joins list; this means it can be appended directly into
556 // any existing query without worrying about whether the developer has remembered to add
557 // whitespace after it.
558 if ($joins) {
559 $joins .= ' ';
562 // Optionally remove the leading comma.
563 if (!$leadingcomma) {
564 $selects = ltrim($selects, ' ,');
567 return (object)['selects' => $selects, 'joins' => $joins, 'params' => $params,
568 'mappings' => $mappings];
572 * Similar to {@see \moodle_database::sql_fullname} except it returns all user name fields as defined by site config, in a
573 * single select statement suitable for inclusion in a query/filter for a users fullname, e.g.
575 * [$select, $params] = fields::get_sql_fullname('u');
576 * $users = $DB->get_records_sql_menu("SELECT u.id, {$select} FROM {user} u", $params);
578 * @param string|null $tablealias User table alias, if set elsewhere in the query, null if not required
579 * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used
580 * @return array SQL select snippet and parameters
582 public static function get_sql_fullname(?string $tablealias = 'u', bool $override = false): array {
583 global $DB;
585 $unique = self::$uniqueidentifier++;
587 $namefields = self::get_name_fields();
589 // Create a dummy user object containing all name fields.
590 $dummyuser = (object) array_combine($namefields, $namefields);
591 $dummyfullname = fullname($dummyuser, $override);
593 // Extract any name fields from the fullname format in the order that they appear.
594 $matchednames = array_values(order_in_string($namefields, $dummyfullname));
595 $namelookup = $namepattern = $elements = $params = [];
597 foreach ($namefields as $index => $namefield) {
598 $namefieldwithalias = $tablealias ? "{$tablealias}.{$namefield}" : $namefield;
600 // Coalesce the name fields to ensure we don't return null.
601 $emptyparam = "uf{$unique}ep_{$index}";
602 $namelookup[$namefield] = "COALESCE({$namefieldwithalias}, :{$emptyparam})";
603 $params[$emptyparam] = '';
605 $namepattern[] = '\b' . preg_quote($namefield) . '\b';
608 // Grab any content between the name fields, inserting them after each name field.
609 $chunks = preg_split('/(' . implode('|', $namepattern) . ')/', $dummyfullname);
610 foreach ($chunks as $index => $chunk) {
611 if ($index > 0) {
612 $elements[] = $namelookup[$matchednames[$index - 1]];
615 if (core_text::strlen($chunk) > 0) {
616 // If content is just whitespace, add to elements directly (also Oracle doesn't support passing ' ' as param).
617 if (preg_match('/^\s+$/', $chunk)) {
618 $elements[] = "'$chunk'";
619 } else {
620 $elementparam = "uf{$unique}fp_{$index}";
621 $elements[] = ":{$elementparam}";
622 $params[$elementparam] = $chunk;
627 return [$DB->sql_concat(...$elements), $params];
631 * Gets the display name of a given user field.
633 * Supports field names from the 'user' database table, and custom profile fields supplied in
634 * the format 'profile_field_xx'.
636 * @param string $field Field name in database
637 * @return string Field name for display to user
638 * @throws \coding_exception
640 public static function get_display_name(string $field): string {
641 global $CFG;
643 // Custom fields have special handling.
644 if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
645 require_once($CFG->dirroot . '/user/profile/lib.php');
646 $fieldinfo = profile_get_custom_field_data_by_shortname($matches[1], false);
647 // Use format_string so it can be translated with multilang filter if necessary.
648 return $fieldinfo ? format_string($fieldinfo->name) : $field;
651 // Some fields have language strings which are not the same as field name.
652 switch ($field) {
653 case 'picture' : {
654 return get_string('pictureofuser');
657 // Otherwise just use the same lang string.
658 return get_string($field);
662 * Resets the unique identifier used to ensure that multiple SQL fragments generated in the
663 * same request will have different identifiers for parameters and table aliases.
665 * This is intended only for use in unit testing.
667 public static function reset_unique_identifier() {
668 self::$uniqueidentifier = 1;
672 * Checks if a field name looks like a custom profile field i.e. it begins with profile_field_
673 * (does not check if that profile field actually exists).
675 * @param string $fieldname Field name
676 * @return string Empty string if not a profile field, or profile field name (without profile_field_)
678 public static function match_custom_field(string $fieldname): string {
679 if (preg_match(self::PROFILE_FIELD_REGEX, $fieldname, $matches)) {
680 return $matches[1];
681 } else {
682 return '';