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