1<?php 2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */ 3 4/** 5 * Auto completion class for user lists 6 */ 7class ilUserAutoComplete 8{ 9 const MAX_ENTRIES = 1000; 10 11 12 /** 13 * @var int 14 */ 15 const SEARCH_TYPE_LIKE = 1; 16 17 /** 18 * @var int 19 */ 20 const SEARCH_TYPE_EQUALS = 2; 21 22 /** 23 * @var int 24 */ 25 const PRIVACY_MODE_RESPECT_USER_SETTING = 1; 26 27 /** 28 * @var int 29 */ 30 const PRIVACY_MODE_IGNORE_USER_SETTING = 2; 31 32 /** 33 * @var ilLogger 34 */ 35 private $logger = null; 36 37 /** 38 * @var bool 39 */ 40 private $searchable_check = false; 41 42 /** 43 * @var bool 44 */ 45 private $user_access_check = true; 46 47 /** 48 * @var array 49 */ 50 private $possible_fields = array(); 51 52 /** 53 * @var string 54 */ 55 private $result_field; 56 57 /** 58 * @var int 59 */ 60 private $search_type; 61 62 /** 63 * @var int 64 */ 65 private $privacy_mode; 66 67 /** 68 * @var ilObjUser 69 */ 70 private $user; 71 72 73 private $limit = 0; 74 75 private $user_limitations = true; 76 77 /** 78 * @var bool 79 */ 80 private $respect_min_search_character_count = true; 81 82 /** 83 * @var bool 84 */ 85 private $more_link_available = false; 86 87 /** 88 * @var callable 89 */ 90 protected $user_filter = null; 91 92 /** 93 * Default constructor 94 */ 95 public function __construct() 96 { 97 global $DIC; 98 99 $this->result_field = 'login'; 100 101 $this->setSearchType(self::SEARCH_TYPE_LIKE); 102 $this->setPrivacyMode(self::PRIVACY_MODE_IGNORE_USER_SETTING); 103 104 $this->logger = $DIC->logger()->user(); 105 } 106 107 /** 108 * @param bool $a_status 109 */ 110 public function respectMinimumSearchCharacterCount($a_status) 111 { 112 $this->respect_min_search_character_count = $a_status; 113 } 114 115 /** 116 * @return bool 117 */ 118 public function getRespectMinimumSearchCharacterCount() 119 { 120 return $this->respect_min_search_character_count; 121 } 122 123 124 /** 125 * Closure for filtering users 126 * e.g 127 * $rep_search_gui->addUserAccessFilterCallable(function($user_ids) use($ref_id,$rbac_perm,$pos_perm)) { 128 * // filter users 129 * return $filtered_users 130 * } 131 * @param callable $user_filter 132 */ 133 public function addUserAccessFilterCallable(callable $user_filter) 134 { 135 $this->user_filter = $user_filter; 136 } 137 138 public function setLimit($a_limit) 139 { 140 $this->limit = $a_limit; 141 } 142 143 public function getLimit() 144 { 145 return $this->limit; 146 } 147 148 /** 149 * @param int $search_type 150 */ 151 public function setSearchType($search_type) 152 { 153 $this->search_type = $search_type; 154 } 155 156 /** 157 * @return mixed 158 */ 159 public function getSearchType() 160 { 161 return $this->search_type; 162 } 163 164 /** 165 * @param int $privacy_mode 166 */ 167 public function setPrivacyMode($privacy_mode) 168 { 169 $this->privacy_mode = $privacy_mode; 170 } 171 172 /** 173 * @return int 174 */ 175 public function getPrivacyMode() 176 { 177 return $this->privacy_mode; 178 } 179 180 /** 181 * @param ilObjUser $user 182 */ 183 public function setUser($user) 184 { 185 $this->user = $user; 186 } 187 188 /** 189 * @return ilObjUser 190 */ 191 public function getUser() 192 { 193 return $this->user; 194 } 195 196 /** 197 * Enable the check whether the field is searchable in Administration -> Settings -> Standard Fields 198 * @param bool $a_status 199 */ 200 public function enableFieldSearchableCheck($a_status) 201 { 202 $this->searchable_check = $a_status; 203 } 204 205 /** 206 * Searchable check enabled 207 * @return bool 208 */ 209 public function isFieldSearchableCheckEnabled() 210 { 211 return $this->searchable_check; 212 } 213 214 /** 215 * Enable user access check. 216 * @see Administration -> User Accounts -> Settings -> General Settings 217 * @param bool $a_status 218 */ 219 public function enableUserAccessCheck($a_status) 220 { 221 $this->user_access_check = $a_status; 222 } 223 224 /** 225 * Check if user access check is enabled 226 * @return bool 227 */ 228 public function isUserAccessCheckEnabled() 229 { 230 return $this->user_access_check; 231 } 232 233 /** 234 * Set searchable fields 235 * @param array $a_fields 236 */ 237 public function setSearchFields($a_fields) 238 { 239 $this->possible_fields = $a_fields; 240 } 241 242 /** 243 * get possible search fields 244 * @return array 245 */ 246 public function getSearchFields() 247 { 248 return $this->possible_fields; 249 } 250 251 /** 252 * Get searchable fields 253 * @return array 254 */ 255 protected function getFields() 256 { 257 if (!$this->isFieldSearchableCheckEnabled()) { 258 return $this->getSearchFields(); 259 } 260 $available_fields = array(); 261 foreach ($this->getSearchFields() as $field) { 262 include_once 'Services/Search/classes/class.ilUserSearchOptions.php'; 263 if (ilUserSearchOptions::_isEnabled($field)) { 264 $available_fields[] = $field; 265 } 266 } 267 return $available_fields; 268 } 269 270 /** 271 * Set result field 272 * @param string $a_field 273 */ 274 public function setResultField($a_field) 275 { 276 $this->result_field = $a_field; 277 } 278 279 /** 280 * Get completion list 281 * @param string $a_str 282 * @return string 283 */ 284 public function getList($a_str) 285 { 286 /** 287 * @var $ilDB ilDB 288 */ 289 global $DIC; 290 291 $ilDB = $DIC['ilDB']; 292 293 $parsed_query = $this->parseQueryString($a_str); 294 295 if (ilStr::strLen($parsed_query['query']) < ilQueryParser::MIN_WORD_LENGTH) { 296 $result_json['items'] = []; 297 $result_json['hasMoreResults'] = false; 298 $this->logger->debug('Autocomplete search rejected: minimum characters count.'); 299 return json_encode($result_json); 300 } 301 302 303 $select_part = $this->getSelectPart(); 304 $where_part = $this->getWherePart($parsed_query); 305 $order_by_part = $this->getOrderByPart(); 306 $query = implode(" ", array( 307 'SELECT ' . $select_part, 308 'FROM ' . $this->getFromPart(), 309 $where_part ? 'WHERE ' . $where_part : '', 310 $order_by_part ? 'ORDER BY ' . $order_by_part : '' 311 )); 312 313 $this->logger->debug('Query: ' . $query); 314 315 $res = $ilDB->query($query); 316 317 // add email only if it is "searchable" 318 $add_email = true; 319 include_once 'Services/Search/classes/class.ilUserSearchOptions.php'; 320 if ($this->isFieldSearchableCheckEnabled() && !ilUserSearchOptions::_isEnabled("email")) { 321 $add_email = false; 322 } 323 324 $add_second_email = true; 325 if ($this->isFieldSearchableCheckEnabled() && !ilUserSearchOptions::_isEnabled("second_email")) { 326 $add_second_email = false; 327 } 328 329 include_once './Services/Search/classes/class.ilSearchSettings.php'; 330 $max = $this->getLimit() ? $this->getLimit() : ilSearchSettings::getInstance()->getAutoCompleteLength(); 331 $cnt = 0; 332 $more_results = false; 333 $result = array(); 334 $recs = array(); 335 $usrIds = array(); 336 while (($rec = $ilDB->fetchAssoc($res)) && $cnt < ($max + 1)) { 337 if ($cnt >= $max && $this->isMoreLinkAvailable()) { 338 $more_results = true; 339 break; 340 } 341 $recs[$rec['usr_id']] = $rec; 342 $usrIds[] = $rec['usr_id']; 343 } 344 if (is_callable($this->user_filter, true, $callable_name = '')) { 345 $usrIds = call_user_func_array($this->user_filter, [$usrIds]); 346 } 347 foreach ($usrIds as $usr_id) { 348 $rec = $recs[$usr_id]; 349 350 if (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || in_array($rec['profile_value'], ['y','g'])) { 351 $label = $rec['lastname'] . ', ' . $rec['firstname'] . ' [' . $rec['login'] . ']'; 352 } else { 353 $label = '[' . $rec['login'] . ']'; 354 } 355 356 if ($add_email && $rec['email'] && (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || 'y' == $rec['email_value'])) { 357 $label .= ', ' . $rec['email']; 358 } 359 360 if ($add_second_email && $rec['second_email'] && (self::PRIVACY_MODE_RESPECT_USER_SETTING != $this->getPrivacyMode() || 'y' == $rec['second_email_value'])) { 361 $label .= ', ' . $rec['second_email']; 362 } 363 364 $result[$cnt]['value'] = (string) $rec[$this->result_field]; 365 $result[$cnt]['label'] = $label; 366 $result[$cnt]['id'] = $rec['usr_id']; 367 $cnt++; 368 } 369 370 include_once 'Services/JSON/classes/class.ilJsonUtil.php'; 371 372 $result_json['items'] = $result; 373 $result_json['hasMoreResults'] = $more_results; 374 375 $this->logger->dump($result_json, ilLogLevel::DEBUG); 376 377 return ilJsonUtil::encode($result_json); 378 } 379 380 /** 381 * @return string 382 */ 383 protected function getSelectPart() 384 { 385 $fields = array( 386 'ud.usr_id', 387 'ud.login', 388 'ud.firstname', 389 'ud.lastname', 390 'ud.email', 391 'ud.second_email' 392 ); 393 394 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) { 395 $fields[] = 'profpref.value profile_value'; 396 $fields[] = 'pubemail.value email_value'; 397 $fields[] = 'pubsecondemail.value second_email_value'; 398 } 399 400 return implode(', ', $fields); 401 } 402 403 /** 404 * @return string 405 */ 406 protected function getFromPart() 407 { 408 /** 409 * @var $ilDB ilDB 410 */ 411 global $DIC; 412 413 $ilDB = $DIC['ilDB']; 414 415 $joins = array(); 416 417 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) { 418 $joins[] = 'LEFT JOIN usr_pref profpref 419 ON profpref.usr_id = ud.usr_id 420 AND profpref.keyword = ' . $ilDB->quote('public_profile', 'text'); 421 422 $joins[] = 'LEFT JOIN usr_pref pubemail 423 ON pubemail.usr_id = ud.usr_id 424 AND pubemail.keyword = ' . $ilDB->quote('public_email', 'text'); 425 426 $joins[] = 'LEFT JOIN usr_pref pubsecondemail 427 ON pubsecondemail.usr_id = ud.usr_id 428 AND pubsecondemail.keyword = ' . $ilDB->quote('public_second_email', 'text'); 429 } 430 431 if ($joins) { 432 return 'usr_data ud ' . implode(' ', $joins); 433 } else { 434 return 'usr_data ud'; 435 } 436 } 437 438 /** 439 * @param string 440 * @return string 441 */ 442 protected function getWherePart(array $search_query) 443 { 444 /** 445 * @var $ilDB ilDB 446 * @var $ilSetting ilSetting 447 */ 448 global $DIC; 449 450 $ilDB = $DIC['ilDB']; 451 $ilSetting = $DIC['ilSetting']; 452 453 $outer_conditions = array(); 454 455 // In 'anonymous' context with respected user privacy, only users with globally published profiles should be found. 456 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode() && 457 $this->getUser() instanceof ilObjUser && 458 $this->getUser()->isAnonymous() 459 ) { 460 if (!$ilSetting->get('enable_global_profiles', 0)) { 461 // If 'Enable User Content Publishing' is not set in the administration, no user should be found for 'anonymous' context. 462 return '1 = 2'; 463 } else { 464 // Otherwise respect the profile activation setting of every user (as a global (outer) condition in the where clause). 465 $outer_conditions[] = 'profpref.value = ' . $ilDB->quote('g', 'text'); 466 } 467 } 468 469 $outer_conditions[] = 'ud.usr_id != ' . $ilDB->quote(ANONYMOUS_USER_ID, 'integer'); 470 471 $field_conditions = array(); 472 foreach ($this->getFields() as $field) { 473 $field_condition = $this->getQueryConditionByFieldAndValue($field, $search_query); 474 475 if ('email' == $field && self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) { 476 // If privacy should be respected, the profile setting of every user concerning the email address has to be 477 // respected (in every user context, no matter if the user is 'logged in' or 'anonymous'). 478 $email_query = array(); 479 $email_query[] = $field_condition; 480 $email_query[] = 'pubemail.value = ' . $ilDB->quote('y', 'text'); 481 $field_conditions[] = '(' . implode(' AND ', $email_query) . ')'; 482 } elseif ('second_email' == $field && self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode()) { 483 // If privacy should be respected, the profile setting of every user concerning the email address has to be 484 // respected (in every user context, no matter if the user is 'logged in' or 'anonymous'). 485 $email_query = array(); 486 $email_query[] = $field_condition; 487 $email_query[] = 'pubsecondemail.value = ' . $ilDB->quote('y', 'text'); 488 $field_conditions[] = '(' . implode(' AND ', $email_query) . ')'; 489 } else { 490 $field_conditions[] = $field_condition; 491 } 492 } 493 494 // If the current user context ist 'logged in' and privacy should be respected, all fields >>>except the login<<< 495 // should only be searchable if the users' profile is published (y oder g) 496 // In 'anonymous' context we do not need this additional conditions, 497 // because we checked the privacy setting in the condition above: profile = 'g' 498 if (self::PRIVACY_MODE_RESPECT_USER_SETTING == $this->getPrivacyMode() && 499 $this->getUser() instanceof ilObjUser && !$this->getUser()->isAnonymous() && 500 $field_conditions 501 ) { 502 $fields = '(' . implode(' OR ', $field_conditions) . ')'; 503 504 $field_conditions = [ 505 '(' . implode(' AND ', array( 506 $fields, 507 $ilDB->in('profpref.value', array('y', 'g'), false, 'text') 508 )) . ')' 509 ]; 510 } 511 512 // The login field must be searchable regardless (for 'logged in' users) of any privacy settings. 513 // We handled the general condition for 'anonymous' context above: profile = 'g' 514 $field_conditions[] = $this->getQueryConditionByFieldAndValue('login', $search_query); 515 516 include_once 'Services/User/classes/class.ilUserAccountSettings.php'; 517 if (ilUserAccountSettings::getInstance()->isUserAccessRestricted()) { 518 include_once './Services/User/classes/class.ilUserFilter.php'; 519 $outer_conditions[] = $ilDB->in('time_limit_owner', ilUserFilter::getInstance()->getFolderIds(), false, 'integer'); 520 } 521 522 if ($field_conditions) { 523 $outer_conditions[] = '(' . implode(' OR ', $field_conditions) . ')'; 524 } 525 526 include_once './Services/Search/classes/class.ilSearchSettings.php'; 527 $settings = ilSearchSettings::getInstance(); 528 529 if (!$settings->isInactiveUserVisible() && $this->getUserLimitations()) { 530 $outer_conditions[] = "ud.active = " . $ilDB->quote(1, 'integer'); 531 } 532 533 if (!$settings->isLimitedUserVisible() && $this->getUserLimitations()) { 534 $unlimited = "ud.time_limit_unlimited = " . $ilDB->quote(1, 'integer'); 535 $from = "ud.time_limit_from < " . $ilDB->quote(time(), 'integer'); 536 $until = "ud.time_limit_until > " . $ilDB->quote(time(), 'integer'); 537 538 $outer_conditions[] = '(' . $unlimited . ' OR (' . $from . ' AND ' . $until . '))'; 539 } 540 541 return implode(' AND ', $outer_conditions); 542 } 543 544 /** 545 * @return string 546 */ 547 protected function getOrderByPart() 548 { 549 return 'login ASC'; 550 } 551 552 /** 553 * @param string $field 554 * @param array $parsed_query 555 * @return string 556 */ 557 protected function getQueryConditionByFieldAndValue($field, $query) 558 { 559 /** 560 * @var $ilDB ilDB 561 */ 562 global $DIC; 563 564 $ilDB = $DIC['ilDB']; 565 566 $query_strings = array($query['query']); 567 568 if (array_key_exists($field, $query)) { 569 $query_strings = array($query[$field]); 570 } elseif (array_key_exists('parts', $query)) { 571 $query_strings = $query['parts']; 572 } 573 574 $query_condition = '( '; 575 $num = 0; 576 foreach ($query_strings as $query_string) { 577 if ($num++ > 0) { 578 $query_condition .= ' OR '; 579 } 580 if (self::SEARCH_TYPE_LIKE == $this->getSearchType()) { 581 $query_condition .= $ilDB->like($field, 'text', $query_string . '%'); 582 } else { 583 $query_condition .= $ilDB->like($field, 'text', $query_string); 584 } 585 } 586 $query_condition .= ')'; 587 return $query_condition; 588 } 589 590 /** 591 * allow user limitations like inactive and access limitations 592 * 593 * @param bool $a_limitations 594 */ 595 public function setUserLimitations($a_limitations) 596 { 597 $this->user_limitations = (bool) $a_limitations; 598 } 599 600 /** 601 * allow user limitations like inactive and access limitations 602 * @return bool 603 */ 604 public function getUserLimitations() 605 { 606 return $this->user_limitations; 607 } 608 609 /** 610 * @return boolean 611 */ 612 public function isMoreLinkAvailable() 613 { 614 return $this->more_link_available; 615 } 616 617 /** 618 * IMPORTANT: remember to read request parameter 'fetchall' to use this function 619 * 620 * @param boolean $more_link_available 621 */ 622 public function setMoreLinkAvailable($more_link_available) 623 { 624 $this->more_link_available = $more_link_available; 625 } 626 627 /** 628 * Parse query string 629 * @param string $a_query 630 * @return $query 631 */ 632 public function parseQueryString($a_query) 633 { 634 $query = array(); 635 636 if (!stristr($a_query, '\\')) { 637 $a_query = str_replace('%', '\%', $a_query); 638 $a_query = str_replace('_', '\_', $a_query); 639 } 640 641 $query['query'] = trim($a_query); 642 643 // "," means fixed search for lastname, firstname 644 if (strpos($a_query, ',')) { 645 $comma_separated = (array) explode(',', $a_query); 646 647 if (count($comma_separated) == 2) { 648 if (trim($comma_separated[0])) { 649 $query['lastname'] = trim($comma_separated[0]); 650 } 651 if (trim($comma_separated[1])) { 652 $query['firstname'] = trim($comma_separated[1]); 653 } 654 } 655 } else { 656 $whitespace_separated = (array) explode(' ', $a_query); 657 foreach ($whitespace_separated as $part) { 658 if (trim($part)) { 659 $query['parts'][] = trim($part); 660 } 661 } 662 } 663 664 $this->logger->dump($query, ilLogLevel::DEBUG); 665 666 return $query; 667 } 668} 669