1 |
efrain |
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 |
namespace core_user;
|
|
|
18 |
|
|
|
19 |
use core_text;
|
|
|
20 |
|
|
|
21 |
/**
|
|
|
22 |
* Class for retrieving information about user fields that are needed for displaying user identity.
|
|
|
23 |
*
|
|
|
24 |
* @package core_user
|
|
|
25 |
*/
|
|
|
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 . '(.*)$~';
|
|
|
31 |
|
|
|
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;
|
|
|
40 |
|
|
|
41 |
/** @var \context|null Context in use */
|
|
|
42 |
protected $context;
|
|
|
43 |
|
|
|
44 |
/** @var bool True to allow custom user fields */
|
|
|
45 |
protected $allowcustom;
|
|
|
46 |
|
|
|
47 |
/** @var bool[] Array of purposes (from PURPOSE_xx to true/false) */
|
|
|
48 |
protected $purposes;
|
|
|
49 |
|
|
|
50 |
/** @var string[] List of extra fields to include */
|
|
|
51 |
protected $include;
|
|
|
52 |
|
|
|
53 |
/** @var string[] List of fields to exclude */
|
|
|
54 |
protected $exclude;
|
|
|
55 |
|
|
|
56 |
/** @var int Unique identifier for different queries generated in same request */
|
|
|
57 |
protected static $uniqueidentifier = 1;
|
|
|
58 |
|
|
|
59 |
/** @var array|null Associative array from field => array of purposes it was used for => true */
|
|
|
60 |
protected $fields = null;
|
|
|
61 |
|
|
|
62 |
/**
|
|
|
63 |
* Protected constructor - use one of the for_xx methods to create an object.
|
|
|
64 |
*
|
|
|
65 |
* @param int $purpose Initial purpose for object or -1 for none
|
|
|
66 |
*/
|
|
|
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,
|
|
|
72 |
];
|
|
|
73 |
if ($purpose != -1) {
|
|
|
74 |
$this->purposes[$purpose] = true;
|
|
|
75 |
}
|
|
|
76 |
$this->include = [];
|
|
|
77 |
$this->exclude = [];
|
|
|
78 |
$this->context = null;
|
|
|
79 |
$this->allowcustom = true;
|
|
|
80 |
}
|
|
|
81 |
|
|
|
82 |
/**
|
|
|
83 |
* Constructs an empty user fields object to get arbitrary user fields.
|
|
|
84 |
*
|
|
|
85 |
* You can add fields to retrieve with the including() function.
|
|
|
86 |
*
|
|
|
87 |
* @return fields User fields object ready for use
|
|
|
88 |
*/
|
|
|
89 |
public static function empty(): fields {
|
|
|
90 |
return new fields();
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
/**
|
|
|
94 |
* Constructs a user fields object to get identity information for display.
|
|
|
95 |
*
|
|
|
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.
|
|
|
99 |
*
|
|
|
100 |
* If the code can only handle fields in the main user table, and not custom profile fields,
|
|
|
101 |
* then set $allowcustom to false.
|
|
|
102 |
*
|
|
|
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:
|
|
|
105 |
*
|
|
|
106 |
* $fields = fields::for_identity($context)->with_userpic()->excluding('email');
|
|
|
107 |
*
|
|
|
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
|
|
|
111 |
*/
|
|
|
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;
|
|
|
117 |
}
|
|
|
118 |
|
|
|
119 |
/**
|
|
|
120 |
* Constructs a user fields object to get information required for displaying a user picture.
|
|
|
121 |
*
|
|
|
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:
|
|
|
124 |
*
|
|
|
125 |
* $fields = fields::for_userpic()->with_name()->excluding('email');
|
|
|
126 |
*
|
|
|
127 |
* @return fields User fields object ready for use
|
|
|
128 |
*/
|
|
|
129 |
public static function for_userpic(): fields {
|
|
|
130 |
return new fields(self::PURPOSE_USERPIC);
|
|
|
131 |
}
|
|
|
132 |
|
|
|
133 |
/**
|
|
|
134 |
* Constructs a user fields object to get information required for displaying a user full name.
|
|
|
135 |
*
|
|
|
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:
|
|
|
138 |
*
|
|
|
139 |
* $fields = fields::for_name()->with_userpic()->excluding('email');
|
|
|
140 |
*
|
|
|
141 |
* @return fields User fields object ready for use
|
|
|
142 |
*/
|
|
|
143 |
public static function for_name(): fields {
|
|
|
144 |
return new fields(self::PURPOSE_NAME);
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
/**
|
|
|
148 |
* On an existing fields object, adds the fields required for displaying user pictures.
|
|
|
149 |
*
|
|
|
150 |
* @return $this Same object for chaining function calls
|
|
|
151 |
*/
|
|
|
152 |
public function with_userpic(): fields {
|
|
|
153 |
$this->purposes[self::PURPOSE_USERPIC] = true;
|
|
|
154 |
return $this;
|
|
|
155 |
}
|
|
|
156 |
|
|
|
157 |
/**
|
|
|
158 |
* On an existing fields object, adds the fields required for displaying user full names.
|
|
|
159 |
*
|
|
|
160 |
* @return $this Same object for chaining function calls
|
|
|
161 |
*/
|
|
|
162 |
public function with_name(): fields {
|
|
|
163 |
$this->purposes[self::PURPOSE_NAME] = true;
|
|
|
164 |
return $this;
|
|
|
165 |
}
|
|
|
166 |
|
|
|
167 |
/**
|
|
|
168 |
* On an existing fields object, adds the fields required for displaying user identity.
|
|
|
169 |
*
|
|
|
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.
|
|
|
173 |
*
|
|
|
174 |
* If the code can only handle fields in the main user table, and not custom profile fields,
|
|
|
175 |
* then set $allowcustom to false.
|
|
|
176 |
*
|
|
|
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
|
|
|
180 |
*/
|
|
|
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;
|
|
|
186 |
}
|
|
|
187 |
|
|
|
188 |
/**
|
|
|
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'.
|
|
|
191 |
*
|
|
|
192 |
* @param string ...$include One or more fields to add
|
|
|
193 |
* @return $this Same object for chaining function calls
|
|
|
194 |
*/
|
|
|
195 |
public function including(string ...$include): fields {
|
|
|
196 |
$this->include = array_merge($this->include, $include);
|
|
|
197 |
return $this;
|
|
|
198 |
}
|
|
|
199 |
|
|
|
200 |
/**
|
|
|
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'.
|
|
|
203 |
*
|
|
|
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.
|
|
|
206 |
*
|
|
|
207 |
* @param string ...$exclude One or more fields to exclude
|
|
|
208 |
* @return $this Same object for chaining function calls
|
|
|
209 |
*/
|
|
|
210 |
public function excluding(...$exclude): fields {
|
|
|
211 |
$this->exclude = array_merge($this->exclude, $exclude);
|
|
|
212 |
return $this;
|
|
|
213 |
}
|
|
|
214 |
|
|
|
215 |
/**
|
|
|
216 |
* Gets an array of all fields that are required for the specified purposes, also taking
|
|
|
217 |
* into account the $includes and $excludes settings.
|
|
|
218 |
*
|
|
|
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'.
|
|
|
221 |
*
|
|
|
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.
|
|
|
224 |
*
|
|
|
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.)
|
|
|
230 |
*
|
|
|
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
|
|
|
234 |
*/
|
|
|
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];
|
|
|
244 |
}
|
|
|
245 |
}
|
|
|
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] = [];
|
|
|
250 |
}
|
|
|
251 |
$this->fields[$field][self::PURPOSE_USERPIC] = true;
|
|
|
252 |
}
|
|
|
253 |
}
|
|
|
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] = [];
|
|
|
258 |
}
|
|
|
259 |
$this->fields[$field][self::PURPOSE_NAME] = true;
|
|
|
260 |
}
|
|
|
261 |
}
|
|
|
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] = [];
|
|
|
266 |
}
|
|
|
267 |
$this->fields[$field][self::CUSTOM_INCLUDE] = true;
|
|
|
268 |
}
|
|
|
269 |
}
|
|
|
270 |
foreach ($this->exclude as $field) {
|
|
|
271 |
unset($this->fields[$field]);
|
|
|
272 |
}
|
|
|
273 |
|
|
|
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;
|
|
|
280 |
}
|
|
|
281 |
}
|
|
|
282 |
$this->fields = $newfields;
|
|
|
283 |
}
|
|
|
284 |
}
|
|
|
285 |
|
|
|
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');
|
|
|
291 |
}
|
|
|
292 |
}
|
|
|
293 |
|
|
|
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;
|
|
|
301 |
}
|
|
|
302 |
}
|
|
|
303 |
}
|
|
|
304 |
return $result;
|
|
|
305 |
} else {
|
|
|
306 |
return array_keys($this->fields);
|
|
|
307 |
}
|
|
|
308 |
}
|
|
|
309 |
|
|
|
310 |
/**
|
|
|
311 |
* Gets fields required for user pictures.
|
|
|
312 |
*
|
|
|
313 |
* The results include only basic field names (columns from the 'user' database table).
|
|
|
314 |
*
|
|
|
315 |
* @return string[] All fields required for user pictures
|
|
|
316 |
*/
|
|
|
317 |
public static function get_picture_fields(): array {
|
|
|
318 |
return ['id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic',
|
|
|
319 |
'middlename', 'alternatename', 'imagealt', 'email'];
|
|
|
320 |
}
|
|
|
321 |
|
|
|
322 |
/**
|
|
|
323 |
* Gets fields required for user names.
|
|
|
324 |
*
|
|
|
325 |
* The results include only basic field names (columns from the 'user' database table).
|
|
|
326 |
*
|
|
|
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.
|
|
|
331 |
*
|
|
|
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
|
|
|
334 |
*/
|
|
|
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;
|
|
|
342 |
}
|
|
|
343 |
}
|
|
|
344 |
|
|
|
345 |
/**
|
|
|
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).
|
|
|
348 |
*
|
|
|
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.
|
|
|
352 |
*
|
|
|
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.
|
|
|
356 |
*
|
|
|
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
|
|
|
361 |
*/
|
|
|
362 |
public static function get_identity_fields(?\context $context, bool $allowcustom = true): array {
|
|
|
363 |
global $CFG;
|
|
|
364 |
|
|
|
365 |
// Only users with permission get the extra fields.
|
|
|
366 |
if ($context && !has_capability('moodle/site:viewuseridentity', $context)) {
|
|
|
367 |
return [];
|
|
|
368 |
}
|
|
|
369 |
|
|
|
370 |
// Split showuseridentity on comma (filter needed in case the showuseridentity is empty).
|
|
|
371 |
$extra = array_filter(explode(',', $CFG->showuseridentity));
|
|
|
372 |
|
|
|
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');
|
|
|
380 |
|
|
|
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);
|
|
|
386 |
}
|
|
|
387 |
}
|
|
|
388 |
if (!$allowed) {
|
|
|
389 |
unset($extra[$key]);
|
|
|
390 |
}
|
|
|
391 |
}
|
|
|
392 |
}
|
|
|
393 |
|
|
|
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);
|
|
|
398 |
|
|
|
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);
|
|
|
408 |
}
|
|
|
409 |
|
|
|
410 |
if (!$canviewhiddenuserfields) {
|
|
|
411 |
// Remove hidden identifiers from the list.
|
|
|
412 |
$extra = array_diff($extra, $hiddenidentifiers);
|
|
|
413 |
}
|
|
|
414 |
}
|
|
|
415 |
|
|
|
416 |
// Re-index the entries and return.
|
|
|
417 |
$extra = array_values($extra);
|
|
|
418 |
return array_map([core_text::class, 'strtolower'], $extra);
|
|
|
419 |
}
|
|
|
420 |
|
|
|
421 |
/**
|
|
|
422 |
* Gets SQL that can be used in a query to get the necessary fields.
|
|
|
423 |
*
|
|
|
424 |
* The result of this function is an object with fields 'selects', 'joins', 'params', and
|
|
|
425 |
* 'mappings'.
|
|
|
426 |
*
|
|
|
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:
|
|
|
429 |
*
|
|
|
430 |
* SELECT (your existing fields)
|
|
|
431 |
* $selects
|
|
|
432 |
* FROM {user} u
|
|
|
433 |
* JOIN (your existing joins)
|
|
|
434 |
* $joins
|
|
|
435 |
*
|
|
|
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.
|
|
|
438 |
*
|
|
|
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.
|
|
|
445 |
*
|
|
|
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.
|
|
|
449 |
*
|
|
|
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.
|
|
|
452 |
*
|
|
|
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.
|
|
|
456 |
*
|
|
|
457 |
* If your code for any reason cannot cope with custom fields then you can turn them off.
|
|
|
458 |
*
|
|
|
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.
|
|
|
463 |
*
|
|
|
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).
|
|
|
467 |
*
|
|
|
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'.
|
|
|
472 |
*
|
|
|
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.)
|
|
|
476 |
*
|
|
|
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
|
|
|
483 |
*/
|
|
|
484 |
public function get_sql(string $alias = '', bool $namedparams = false, string $prefix = '',
|
|
|
485 |
string $renameid = '', bool $leadingcomma = true): \stdClass {
|
|
|
486 |
global $DB;
|
|
|
487 |
|
|
|
488 |
$fields = $this->get_required_fields();
|
|
|
489 |
|
|
|
490 |
$selects = '';
|
|
|
491 |
$joins = '';
|
|
|
492 |
$params = [];
|
|
|
493 |
$mappings = [];
|
|
|
494 |
|
|
|
495 |
$unique = self::$uniqueidentifier++;
|
|
|
496 |
$fieldcount = 0;
|
|
|
497 |
|
|
|
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;
|
|
|
509 |
}
|
|
|
510 |
}
|
|
|
511 |
if ($gotcustomfields) {
|
|
|
512 |
$usertable = '{user}.';
|
|
|
513 |
} else {
|
|
|
514 |
$usertable = '';
|
|
|
515 |
}
|
|
|
516 |
}
|
|
|
517 |
|
|
|
518 |
foreach ($fields as $field) {
|
|
|
519 |
if (preg_match(self::PROFILE_FIELD_REGEX, $field, $matches)) {
|
|
|
520 |
// Custom profile field.
|
|
|
521 |
$shortname = $matches[1];
|
|
|
522 |
|
|
|
523 |
$fieldcount++;
|
|
|
524 |
|
|
|
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;
|
|
|
534 |
}
|
|
|
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";
|
|
|
550 |
}
|
|
|
551 |
$mappings[$field] = "$usertable$field";
|
|
|
552 |
}
|
|
|
553 |
}
|
|
|
554 |
|
|
|
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 .= ' ';
|
|
|
560 |
}
|
|
|
561 |
|
|
|
562 |
// Optionally remove the leading comma.
|
|
|
563 |
if (!$leadingcomma) {
|
|
|
564 |
$selects = ltrim($selects, ' ,');
|
|
|
565 |
}
|
|
|
566 |
|
|
|
567 |
return (object)['selects' => $selects, 'joins' => $joins, 'params' => $params,
|
|
|
568 |
'mappings' => $mappings];
|
|
|
569 |
}
|
|
|
570 |
|
|
|
571 |
/**
|
|
|
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.
|
|
|
574 |
*
|
|
|
575 |
* [$select, $params] = fields::get_sql_fullname('u');
|
|
|
576 |
* $users = $DB->get_records_sql_menu("SELECT u.id, {$select} FROM {user} u", $params);
|
|
|
577 |
*
|
|
|
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
|
|
|
581 |
*/
|
|
|
582 |
public static function get_sql_fullname(?string $tablealias = 'u', bool $override = false): array {
|
|
|
583 |
global $DB;
|
|
|
584 |
|
|
|
585 |
$unique = self::$uniqueidentifier++;
|
|
|
586 |
|
|
|
587 |
$namefields = self::get_name_fields();
|
|
|
588 |
|
|
|
589 |
// Create a dummy user object containing all name fields.
|
|
|
590 |
$dummyuser = (object) array_combine($namefields, $namefields);
|
|
|
591 |
$dummyfullname = fullname($dummyuser, $override);
|
|
|
592 |
|
|
|
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 = [];
|
|
|
596 |
|
|
|
597 |
foreach ($namefields as $index => $namefield) {
|
|
|
598 |
$namefieldwithalias = $tablealias ? "{$tablealias}.{$namefield}" : $namefield;
|
|
|
599 |
|
|
|
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] = '';
|
|
|
604 |
|
|
|
605 |
$namepattern[] = '\b' . preg_quote($namefield) . '\b';
|
|
|
606 |
}
|
|
|
607 |
|
|
|
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]];
|
|
|
613 |
}
|
|
|
614 |
|
|
|
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;
|
|
|
623 |
}
|
|
|
624 |
}
|
|
|
625 |
}
|
|
|
626 |
|
|
|
627 |
return [$DB->sql_concat(...$elements), $params];
|
|
|
628 |
}
|
|
|
629 |
|
|
|
630 |
/**
|
|
|
631 |
* Gets the display name of a given user field.
|
|
|
632 |
*
|
|
|
633 |
* Supports field names from the 'user' database table, and custom profile fields supplied in
|
|
|
634 |
* the format 'profile_field_xx'.
|
|
|
635 |
*
|
|
|
636 |
* @param string $field Field name in database
|
|
|
637 |
* @return string Field name for display to user
|
|
|
638 |
* @throws \coding_exception
|
|
|
639 |
*/
|
|
|
640 |
public static function get_display_name(string $field): string {
|
|
|
641 |
global $CFG;
|
|
|
642 |
|
|
|
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;
|
|
|
649 |
}
|
|
|
650 |
|
|
|
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');
|
|
|
655 |
}
|
|
|
656 |
}
|
|
|
657 |
// Otherwise just use the same lang string.
|
|
|
658 |
return get_string($field);
|
|
|
659 |
}
|
|
|
660 |
|
|
|
661 |
/**
|
|
|
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.
|
|
|
664 |
*
|
|
|
665 |
* This is intended only for use in unit testing.
|
|
|
666 |
*/
|
|
|
667 |
public static function reset_unique_identifier() {
|
|
|
668 |
self::$uniqueidentifier = 1;
|
|
|
669 |
}
|
|
|
670 |
|
|
|
671 |
/**
|
|
|
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).
|
|
|
674 |
*
|
|
|
675 |
* @param string $fieldname Field name
|
|
|
676 |
* @return string Empty string if not a profile field, or profile field name (without profile_field_)
|
|
|
677 |
*/
|
|
|
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 '';
|
|
|
683 |
}
|
|
|
684 |
}
|
|
|
685 |
}
|