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