ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilUserAutoComplete.php
Go to the documentation of this file.
1<?php
2
23{
24 public const MAX_ENTRIES = 1000;
25 public const SEARCH_TYPE_LIKE = 1;
26 public const SEARCH_TYPE_EQUALS = 2;
29
30 private ?ilLogger $logger = null;
31 private bool $searchable_check = false;
32 private bool $user_access_check = true;
33 private array $possible_fields = []; // Missing array type.
34 private string $result_field;
35 private int $search_type;
36 private int $privacy_mode;
37 private ?ilObjUser $user = null;
38 private int $limit = 0;
39 private bool $user_limitations = true;
41 private bool $more_link_available = false;
42 protected ?Closure $user_filter = null;
43
44 public function __construct()
45 {
46 global $DIC;
47
48 $this->result_field = 'login';
49
50 $this->setSearchType(self::SEARCH_TYPE_LIKE);
51 $this->setPrivacyMode(self::PRIVACY_MODE_IGNORE_USER_SETTING);
52
53 $this->logger = $DIC->logger()->user();
54 }
55
56 public function respectMinimumSearchCharacterCount(bool $a_status): void
57 {
58 $this->respect_min_search_character_count = $a_status;
59 }
60
62 {
64 }
65
74 public function addUserAccessFilterCallable(Closure $user_filter): void
75 {
76 $this->user_filter = $user_filter;
77 }
78
79 public function setLimit(int $a_limit): void
80 {
81 $this->limit = $a_limit;
82 }
83
84 public function getLimit(): int
85 {
86 return $this->limit;
87 }
88
89 public function setSearchType(int $search_type): void
90 {
91 $this->search_type = $search_type;
92 }
93
94 public function getSearchType(): int
95 {
96 return $this->search_type;
97 }
98
99 public function setPrivacyMode(int $privacy_mode): void
100 {
101 $this->privacy_mode = $privacy_mode;
102 }
103
104 public function getPrivacyMode(): int
105 {
106 return $this->privacy_mode;
107 }
108
109 public function setUser(ilObjUser $user): void
110 {
111 $this->user = $user;
112 }
113
114 public function getUser(): ?ilObjUser
115 {
116 return $this->user;
117 }
118
122 public function enableFieldSearchableCheck(bool $a_status): void
123 {
124 $this->searchable_check = $a_status;
125 }
126
127 public function isFieldSearchableCheckEnabled(): bool
128 {
130 }
131
136 public function enableUserAccessCheck(bool $a_status): void
137 {
138 $this->user_access_check = $a_status;
139 }
140
144 public function isUserAccessCheckEnabled(): bool
145 {
147 }
148
152 public function setSearchFields(array $a_fields): void // Missing array type.
153 {
154 $this->possible_fields = $a_fields;
155 }
156
160 public function getSearchFields(): array // Missing array type.
161 {
163 }
164
168 protected function getFields(): array // Missing array type.
169 {
170 if (!$this->isFieldSearchableCheckEnabled()) {
171 return $this->getSearchFields();
172 }
173 $available_fields = [];
174 foreach ($this->getSearchFields() as $field) {
176 $available_fields[] = $field;
177 }
178 }
179 return $available_fields;
180 }
181
185 public function setResultField(string $a_field): void
186 {
187 $this->result_field = $a_field;
188 }
189
193 public function getList(string $a_str): string
194 {
195 global $DIC;
196 $ilDB = $DIC->database();
197
198 $parsed_query = $this->parseQueryString($a_str);
199
200 if (ilStr::strLen($parsed_query['query']) < ilQueryParser::MIN_WORD_LENGTH) {
201 $result_json['items'] = [];
202 $result_json['hasMoreResults'] = false;
203 $this->logger->debug('Autocomplete search rejected: minimum characters count.');
204 return json_encode($result_json);
205 }
206
207
208 $select_part = $this->getSelectPart();
209 $where_part = $this->getWherePart($parsed_query);
210 $order_by_part = $this->getOrderByPart();
211 $query = implode(" ", [
212 'SELECT ' . $select_part,
213 'FROM ' . $this->getFromPart(),
214 $where_part ? 'WHERE ' . $where_part : '',
215 $order_by_part ? 'ORDER BY ' . $order_by_part : ''
216 ]);
217
218 $this->logger->debug('Query: ' . $query);
219
220 $res = $ilDB->query($query);
221
222 // add email only if it is "searchable"
223 $add_email = true;
225 $add_email = false;
226 }
227
228 $add_second_email = true;
229 if ($this->isFieldSearchableCheckEnabled() && !ilUserSearchOptions::_isEnabled("second_email")) {
230 $add_second_email = false;
231 }
232
233 $max = $this->getLimit() ?: ilSearchSettings::getInstance()->getAutoCompleteLength();
234 $cnt = 0;
235 $more_results = false;
236 $result = [];
237 $recs = [];
238 $usrIds = [];
239 while (($rec = $ilDB->fetchAssoc($res)) && $cnt < ($max + 1)) {
240 if ($cnt >= $max && $this->isMoreLinkAvailable()) {
241 $more_results = true;
242 break;
243 }
244 $recs[$rec['usr_id']] = $rec;
245 $usrIds[] = $rec['usr_id'];
246 $cnt++;
247 }
248 $callable_name = null;
249 if (is_callable($this->user_filter, true, $callable_name)) {
250 $usrIds = call_user_func($this->user_filter, $usrIds);
251 }
252 foreach ($usrIds as $usr_id) {
253 $rec = $recs[$usr_id];
254
255 if (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || in_array($rec['profile_value'], ['y','g'])) {
256 $label = $rec['lastname'] . ', ' . $rec['firstname'] . ' [' . $rec['login'] . ']';
257 } else {
258 $label = '[' . $rec['login'] . ']';
259 }
260
261 if ($add_email && $rec['email'] && (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || 'y' == $rec['email_value'])) {
262 $label .= ', ' . $rec['email'];
263 }
264
265 if ($add_second_email && $rec['second_email'] && (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || 'y' == $rec['second_email_value'])) {
266 $label .= ', ' . $rec['second_email'];
267 }
268
269 $result[$cnt]['value'] = (string) $rec[$this->result_field];
270 $result[$cnt]['label'] = $label;
271 $result[$cnt]['id'] = $rec['usr_id'];
272 $cnt++;
273 }
274
275 $result_json['items'] = $result;
276 $result_json['hasMoreResults'] = $more_results;
277
278 $this->logger->dump($result_json, ilLogLevel::DEBUG);
279
280 return json_encode($result_json, JSON_THROW_ON_ERROR);
281 }
282
283 protected function getSelectPart(): string
284 {
285 $fields = [
286 'ud.usr_id',
287 'ud.login',
288 'ud.firstname',
289 'ud.lastname',
290 'ud.email',
291 'ud.second_email'
292 ];
293
294 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) {
295 $fields[] = 'profpref.value profile_value';
296 $fields[] = 'pubemail.value email_value';
297 $fields[] = 'pubsecondemail.value second_email_value';
298 }
299
300 return implode(', ', $fields);
301 }
302
303 protected function getFromPart(): string
304 {
305 global $DIC;
306
307 $ilDB = $DIC->database();
308
309 $joins = [];
310
311 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) {
312 $joins[] = 'LEFT JOIN usr_pref profpref
313 ON profpref.usr_id = ud.usr_id
314 AND profpref.keyword = ' . $ilDB->quote('public_profile', 'text');
315
316 $joins[] = 'LEFT JOIN usr_pref pubemail
317 ON pubemail.usr_id = ud.usr_id
318 AND pubemail.keyword = ' . $ilDB->quote('public_email', 'text');
319
320 $joins[] = 'LEFT JOIN usr_pref pubsecondemail
321 ON pubsecondemail.usr_id = ud.usr_id
322 AND pubsecondemail.keyword = ' . $ilDB->quote('public_second_email', 'text');
323 }
324
325 if ($joins) {
326 return 'usr_data ud ' . implode(' ', $joins);
327 } else {
328 return 'usr_data ud';
329 }
330 }
331
332 protected function getWherePart(array $search_query): string // Missing array type.
333 {
334 global $DIC;
335
336 $ilDB = $DIC->database();
337 $ilSetting = $DIC->settings();
338
339 $outer_conditions = [];
340
341 // In 'anonymous' context with respected user privacy, only users with globally published profiles should be found.
342 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode() &&
343 $this->getUser() instanceof ilObjUser &&
344 $this->getUser()->isAnonymous()
345 ) {
346 if (!$ilSetting->get('enable_global_profiles', '0')) {
347 // If 'Enable User Content Publishing' is not set in the administration, no user should be found for 'anonymous' context.
348 return '1 = 2';
349 } else {
350 // Otherwise respect the profile activation setting of every user (as a global (outer) condition in the where clause).
351 $outer_conditions[] = 'profpref.value = ' . $ilDB->quote('g', 'text');
352 }
353 }
354
355 $outer_conditions[] = 'ud.usr_id != ' . $ilDB->quote(ANONYMOUS_USER_ID, 'integer');
356
357 $field_conditions = [];
358 foreach ($this->getFields() as $field) {
359 $field_condition = $this->getQueryConditionByFieldAndValue($field, $search_query);
360
361 if ('email' == $field && self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) {
362 // If privacy should be respected, the profile setting of every user concerning the email address has to be
363 // respected (in every user context, no matter if the user is 'logged in' or 'anonymous').
364 $email_query = [];
365 $email_query[] = $field_condition;
366 $email_query[] = 'pubemail.value = ' . $ilDB->quote('y', 'text');
367 $field_conditions[] = '(' . implode(' AND ', $email_query) . ')';
368 } elseif ('second_email' == $field && self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) {
369 // If privacy should be respected, the profile setting of every user concerning the email address has to be
370 // respected (in every user context, no matter if the user is 'logged in' or 'anonymous').
371 $email_query = [];
372 $email_query[] = $field_condition;
373 $email_query[] = 'pubsecondemail.value = ' . $ilDB->quote('y', 'text');
374 $field_conditions[] = '(' . implode(' AND ', $email_query) . ')';
375 } else {
376 $field_conditions[] = $field_condition;
377 }
378 }
379
380 // If the current user context ist 'logged in' and privacy should be respected, all fields >>>except the login<<<
381 // should only be searchable if the users' profile is published (y oder g)
382 // In 'anonymous' context we do not need this additional conditions,
383 // because we checked the privacy setting in the condition above: profile = 'g'
384 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode() &&
385 $this->getUser() instanceof ilObjUser && !$this->getUser()->isAnonymous() &&
386 $field_conditions
387 ) {
388 $fields = '(' . implode(' OR ', $field_conditions) . ')';
389
390 $field_conditions = [
391 '(' . implode(' AND ', [
392 $fields,
393 $ilDB->in('profpref.value', ['y', 'g'], false, 'text')
394 ]) . ')'
395 ];
396 }
397
398 // The login field must be searchable regardless (for 'logged in' users) of any privacy settings.
399 // We handled the general condition for 'anonymous' context above: profile = 'g'
400 $field_conditions[] = $this->getQueryConditionByFieldAndValue('login', $search_query);
401
402 if (ilUserAccountSettings::getInstance()->isUserAccessRestricted()) {
403 $outer_conditions[] = $ilDB->in('time_limit_owner', ilUserFilter::getInstance()->getFolderIds(), false, 'integer');
404 }
405
406 if ($field_conditions) {
407 $outer_conditions[] = '(' . implode(' OR ', $field_conditions) . ')';
408 }
409
410 $settings = ilSearchSettings::getInstance();
411
412 if (!$settings->isInactiveUserVisible() && $this->getUserLimitations()) {
413 $outer_conditions[] = "ud.active = " . $ilDB->quote(1, 'integer');
414 }
415
416 if (!$settings->isLimitedUserVisible() && $this->getUserLimitations()) {
417 $unlimited = "ud.time_limit_unlimited = " . $ilDB->quote(1, 'integer');
418 $from = "ud.time_limit_from < " . $ilDB->quote(time(), 'integer');
419 $until = "ud.time_limit_until > " . $ilDB->quote(time(), 'integer');
420
421 $outer_conditions[] = '(' . $unlimited . ' OR (' . $from . ' AND ' . $until . '))';
422 }
423
424 return implode(' AND ', $outer_conditions);
425 }
426
427 protected function getOrderByPart(): string
428 {
429 return 'login ASC';
430 }
431
432 protected function getQueryConditionByFieldAndValue(string $field, array $query): string // Missing array type.
433 {
434 global $DIC;
435
436 $ilDB = $DIC->database();
437
438 $query_strings = [$query['query']];
439
440 if (array_key_exists($field, $query)) {
441 $query_strings = [$query[$field]];
442 } elseif (array_key_exists('parts', $query)) {
443 $query_strings = $query['parts'];
444 }
445
446 $query_condition = '( ';
447 $num = 0;
448 foreach ($query_strings as $query_string) {
449 if ($num++ > 0) {
450 $query_condition .= ' OR ';
451 }
452 if (self::SEARCH_TYPE_LIKE == $this->getSearchType()) {
453 $query_condition .= $ilDB->like($field, 'text', $query_string . '%');
454 } else {
455 $query_condition .= $ilDB->like($field, 'text', $query_string);
456 }
457 }
458 $query_condition .= ')';
459 return $query_condition;
460 }
461
465 public function setUserLimitations(bool $a_limitations): void
466 {
467 $this->user_limitations = $a_limitations;
468 }
469
473 public function getUserLimitations(): bool
474 {
476 }
477
478 public function isMoreLinkAvailable(): bool
479 {
481 }
482
486 public function setMoreLinkAvailable(bool $more_link_available): void
487 {
488 $this->more_link_available = $more_link_available;
489 }
490
494 public function parseQueryString(string $a_query): array // Missing array type.
495 {
496 $query = [];
497
498 if (strpos($a_query, '\\') === false) {
499 $a_query = str_replace(['%', '_'], ['\%', '\_'], $a_query);
500 }
501
502 $query['query'] = trim($a_query);
503
504 // "," means fixed search for lastname, firstname
505 if (strpos($a_query, ',')) {
506 $comma_separated = explode(',', $a_query);
507
508 if (count($comma_separated) == 2) {
509 if (trim($comma_separated[0])) {
510 $query['lastname'] = trim($comma_separated[0]);
511 }
512 if (trim($comma_separated[1])) {
513 $query['firstname'] = trim($comma_separated[1]);
514 }
515 }
516 } else {
517 $whitespace_separated = explode(' ', $a_query);
518 foreach ($whitespace_separated as $part) {
519 if (trim($part)) {
520 $query['parts'][] = trim($part);
521 }
522 }
523 }
524
525 $this->logger->dump($query, ilLogLevel::DEBUG);
526
527 return $query;
528 }
529}
Component logger with individual log levels by component id.
User class.
const MIN_WORD_LENGTH
Minimum of characters required for search.
static strLen(string $a_string)
Definition: class.ilStr.php:60
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getFields()
Get searchable fields.
setSearchFields(array $a_fields)
Set searchable fields.
setResultField(string $a_field)
Set result field.
setUserLimitations(bool $a_limitations)
allow user limitations like inactive and access limitations
setPrivacyMode(int $privacy_mode)
respectMinimumSearchCharacterCount(bool $a_status)
getQueryConditionByFieldAndValue(string $field, array $query)
getUserLimitations()
allow user limitations like inactive and access limitations
getList(string $a_str)
Get completion list.
isUserAccessCheckEnabled()
Check if user access check is enabled.
setSearchType(int $search_type)
getSearchFields()
get possible search fields
enableUserAccessCheck(bool $a_status)
Enable user access check.
addUserAccessFilterCallable(Closure $user_filter)
Closure for filtering users e.g $rep_search_gui->addUserAccessFilterCallable(function($user_ids) use(...
setMoreLinkAvailable(bool $more_link_available)
IMPORTANT: remember to read request parameter 'fetchall' to use this function.
enableFieldSearchableCheck(bool $a_status)
Enable the check whether the field is searchable in Administration -> Settings -> Standard Fields.
parseQueryString(string $a_query)
Parse query string.
getWherePart(array $search_query)
const ANONYMOUS_USER_ID
Definition: constants.php:27
$res
Definition: ltiservices.php:69
global $ilSetting
Definition: privfeed.php:31
global $DIC
Definition: shib_login.php:26