1<?php
2/*
3    +-----------------------------------------------------------------------------+
4    | ILIAS open source                                                           |
5    +-----------------------------------------------------------------------------+
6    | Copyright (c) 1998-2001 ILIAS open source, University of Cologne            |
7    |                                                                             |
8    | This program is free software; you can redistribute it and/or               |
9    | modify it under the terms of the GNU General Public License                 |
10    | as published by the Free Software Foundation; either version 2              |
11    | of the License, or (at your option) any later version.                      |
12    |                                                                             |
13    | This program is distributed in the hope that it will be useful,             |
14    | but WITHOUT ANY WARRANTY; without even the implied warranty of              |
15    | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               |
16    | GNU General Public License for more details.                                |
17    |                                                                             |
18    | You should have received a copy of the GNU General Public License           |
19    | along with this program; if not, write to the Free Software                 |
20    | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. |
21    +-----------------------------------------------------------------------------+
22*/
23
24/**
25* searchResult stores all result of a search query.
26* Offers methods like mergeResults. To merge result sets of different queries.
27*
28*
29* @author Stefan Meyer <meyer@leifos.com>
30* @version Id$
31*
32* @package ilias-search
33*/
34include_once('Services/Search/classes/class.ilUserSearchCache.php');
35
36define('DEFAULT_SEARCH', 0);
37define('ADVANCED_SEARCH', 1);
38define('ADVANCED_MD_SEARCH', 4);
39
40class ilSearchResult
41{
42    public $permission = 'visible';
43
44    public $user_id;
45    public $entries = array();
46    public $results = array();
47    public $observers = array();
48
49    protected $search_cache = null;
50    protected $offset = 0;
51
52    // OBJECT VARIABLES
53    public $ilias;
54    public $ilAccess;
55
56    // Stores info if MAX HITS is reached or not
57    public $limit_reached = false;
58    public $result;
59
60    protected $preventOverwritingMaxhits = false;
61
62    /**
63    * Constructor
64    * @access	public
65    */
66    public function __construct($a_user_id = 0)
67    {
68        global $DIC;
69
70        $ilias = $DIC['ilias'];
71        $ilAccess = $DIC['ilAccess'];
72        $ilDB = $DIC['ilDB'];
73        $ilUser = $DIC['ilUser'];
74
75        $this->ilAccess = $ilAccess;
76        if ($a_user_id) {
77            $this->user_id = $a_user_id;
78        } else {
79            $this->user_id = $ilUser->getId();
80        }
81        $this->__initSearchSettingsObject();
82        $this->initUserSearchCache();
83
84        $this->db = $ilDB;
85    }
86
87    /**
88    * Set the required permission for the rbac checks in function 'filter()'
89    */
90    public function setRequiredPermission($a_permission)
91    {
92        $this->permission = $a_permission;
93    }
94
95    public function getRequiredPermission()
96    {
97        return $this->permission;
98    }
99
100
101    public function setUserId($a_user_id)
102    {
103        $this->user_id = $a_user_id;
104    }
105    public function getUserId()
106    {
107        return $this->user_id;
108    }
109
110    public function getEntries()
111    {
112        return $this->entries ? $this->entries : array();
113    }
114
115    public function isLimitReached()
116    {
117        return $this->limit_reached ? true : false;
118    }
119
120    public function setMaxHits($a_max_hits)
121    {
122        $this->max_hits = $a_max_hits;
123    }
124    public function getMaxHits()
125    {
126        return $this->max_hits;
127    }
128
129    /**
130     * Check if offset is reached
131     *
132     * @access public
133     * @param int current counter of result
134     * @return bool reached or not
135     */
136    public function isOffsetReached($a_counter)
137    {
138        return ($a_counter < $this->offset) ? false : true;
139    }
140
141    /**
142     *
143     * add search result entry
144     * Entries are stored with 'obj_id'. This method is typically called to store db query results.
145     * @param integer object object_id
146     * @param string obj_type 'lm' or 'crs' ...
147     * @param array value position of query parser words in query string
148     * @param integer child id e.g id of page or chapter
149     * @access	public
150     */
151    public function addEntry($a_obj_id, $a_type, $found, $a_child_id = 0)
152    {
153        // Create new entry if it not exists
154        if (!$this->entries[$a_obj_id]) {
155            $this->entries[$a_obj_id]['obj_id'] = $a_obj_id;
156            $this->entries[$a_obj_id]['type'] = $a_type;
157            $this->entries[$a_obj_id]['found'] = $found;
158            $this->entries[$a_obj_id]['child'] = [];
159
160            if ($a_child_id and $a_child_id != $a_obj_id) {
161                $this->entries[$a_obj_id]['child'][$a_child_id] = $a_child_id;
162            }
163        } else {
164            // replace or add child ('pg','st') id
165            if ($a_child_id and $a_child_id != $a_obj_id) {
166                $this->entries[$a_obj_id]['child'][$a_child_id] = $a_child_id;
167            }
168
169            // UPDATE FOUND
170            $counter = 0;
171            foreach ($found as $position) {
172                if ($position) {
173                    $this->entries[$a_obj_id]['found'][$counter] = $position;
174                }
175                $counter++;
176            }
177        }
178        return true;
179    }
180
181    /**
182     *
183     * Check number of entries
184     * @access	public
185     */
186    public function numEntries()
187    {
188        return count($this->getEntries());
189    }
190
191    /**
192     *
193     * merge entries of this instance and another result object
194     * @param object result_obj
195     * @access	public
196     */
197    public function mergeEntries(&$result_obj)
198    {
199        foreach ($result_obj->getEntries() as $entry) {
200            $this->addEntry($entry['obj_id'], $entry['type'], $entry['found']);
201            $this->__updateEntryChilds($entry['obj_id'], $entry['child']);
202        }
203        return true;
204    }
205
206    /**
207     *
208     * diff entries of this instance and another result object
209     * Used for search in results
210     * @param object result_obj
211     * @access	public
212     */
213    public function diffEntriesFromResult(&$result_obj)
214    {
215        $new_entries = $this->getEntries();
216        $this->entries = array();
217
218        // Get all checked objects
219        foreach ($this->search_cache->getCheckedItems() as $ref_id => $obj_id) {
220            if (isset($new_entries[$obj_id])) {
221                $this->addEntry(
222                    $new_entries[$obj_id]['obj_id'],
223                    $new_entries[$obj_id]['type'],
224                    $new_entries[$obj_id]['found']
225                );
226                $this->__updateEntryChilds(
227                    $new_entries[$obj_id]['obj_id'],
228                    $new_entries[$obj_id]['child']
229                );
230            }
231        }
232    }
233
234    /**
235     *
236     * Build intersection of entries (all entries that are present in both result sets)
237     * @param object result_obj
238     * @access	public
239     */
240    public function intersectEntries(&$result_obj)
241    {
242        $new_entries = $this->getEntries();
243        $this->entries = array();
244
245        foreach ($result_obj->getEntries() as $entry) {
246            $obj_id = $entry['obj_id'];
247            if (isset($new_entries[$obj_id])) {
248                $this->addEntry(
249                    $new_entries[$obj_id]['obj_id'],
250                    $new_entries[$obj_id]['type'],
251                    $new_entries[$obj_id]['found']
252                );
253
254                $this->__updateEntryChilds(
255                    $new_entries[$obj_id]['obj_id'],
256                    $new_entries[$obj_id]['child']
257                );
258            }
259        }
260    }
261
262
263    /**
264     *
265     * add search result
266     * Results are stored with 'ref_id'. This method is typically called after checking access of entries.
267     * @param integer ref_id
268     * @param integer obj_id
269     * @param string obj_type 'lm' or 'crs' ...
270     * @access	public
271     */
272    public function addResult($a_ref_id, $a_obj_id, $a_type)
273    {
274        $this->results[$a_ref_id]['ref_id'] = $a_ref_id;
275        $this->results[$a_ref_id]['obj_id'] = $a_obj_id;
276        $this->results[$a_ref_id]['type'] = $a_type;
277    }
278
279    public function getResults()
280    {
281        return $this->results ? $this->results : array();
282    }
283
284    /**
285     * get result ids
286     *
287     * @access public
288     * @return array result ids
289     */
290    public function getResultIds()
291    {
292        foreach ($this->getResults() as $id => $tmp) {
293            $ids[] = $id;
294        }
295        return $ids ? $ids : array();
296    }
297
298    public function getResultsByObjId()
299    {
300        $tmp_res = array();
301        foreach ($this->getResults() as $ref_id => $res_data) {
302            $tmp_res[$res_data['obj_id']][] = $ref_id;
303        }
304        return $tmp_res ? $tmp_res : array();
305    }
306
307
308    /**
309     *
310     * Get unique results. Return an array of obj_id (No multiple results for references)
311     * Results are stored with 'ref_id'. This method is typically called after checking access of entries.
312     * @access	public
313     */
314    public function getUniqueResults()
315    {
316        $obj_ids = array();
317        foreach ($this->results as $result) {
318            if (in_array($result['obj_id'], $obj_ids)) {
319                continue;
320            }
321            $obj_ids[] = $result['obj_id'];
322            $objects[] = $result;
323        }
324        return $objects ? $objects : array();
325    }
326
327    public function getResultsForPresentation()
328    {
329        $res = array();
330
331        foreach ($this->getResults() as $result) {
332            $res[$result['ref_id']] = $result['obj_id'];
333        }
334        return $res;
335    }
336
337    public function getSubitemIds()
338    {
339        $res = array();
340        foreach ($this->getResults() as $row) {
341            $res[$row['obj_id']] = $row['child'];
342        }
343        return $res ? $res : array();
344    }
345
346
347
348    /**
349     * Filter search result.
350     * Do RBAC checks.
351     *
352     * Allows paging of results for referenced objects
353     *
354     * @access public
355     * @param int root node id
356     * @param bool check and boolean search
357     * @return bool success status
358     *
359     */
360    public function filter($a_root_node, $check_and)
361    {
362        global $DIC;
363
364        $tree = $DIC['tree'];
365
366        // get ref_ids and check access
367        $counter = 0;
368        $offset_counter = 0;
369        foreach ($this->getEntries() as $entry) {
370            // boolean and failed continue
371            if ($check_and and in_array(0, $entry['found'])) {
372                continue;
373            }
374            // Types like role, rolt, user do not need rbac checks
375            $type = ilObject::_lookupType($entry['obj_id']);
376            if ($type == 'rolt' or $type == 'usr' or $type == 'role') {
377                if ($this->callListeners($entry['obj_id'], $entry)) {
378                    $this->addResult($entry['obj_id'], $entry['obj_id'], $type);
379                    if (is_array($entry['child'])) {
380                        $counter += count($entry['child']);
381                    }
382                    // Stop if maximum of hits is reached
383                    if (++$counter > $this->getMaxHits()) {
384                        $this->limit_reached = true;
385                        return true;
386                    }
387                }
388                continue;
389            }
390            // Check referenced objects
391            foreach (ilObject::_getAllReferences($entry['obj_id']) as $ref_id) {
392                // Failed check: if ref id check is failed by previous search
393                if ($this->search_cache->isFailed($ref_id)) {
394                    continue;
395                }
396                // Offset check
397                if ($this->search_cache->isChecked($ref_id) and !$this->isOffsetReached($offset_counter)) {
398                    ++$offset_counter;
399                    continue;
400                }
401
402                if (!$this->callListeners($ref_id, $entry)) {
403                    continue;
404                }
405
406
407
408                // RBAC check
409                $type = ilObject::_lookupType($ref_id, true);
410                if ($this->ilAccess->checkAccessOfUser(
411                    $this->getUserId(),
412                    $this->getRequiredPermission(),
413                    '',
414                    $ref_id,
415                    $type,
416                    $entry['obj_id']
417                )) {
418                    if ($a_root_node == ROOT_FOLDER_ID or $tree->isGrandChild($a_root_node, $ref_id)) {
419                        // Call listeners
420                        #if($this->callListeners($ref_id,$entry))
421                        if (1) {
422                            $this->addResult($ref_id, $entry['obj_id'], $type);
423                            $this->search_cache->appendToChecked($ref_id, $entry['obj_id']);
424                            $this->__updateResultChilds($ref_id, $entry['child']);
425
426                            $counter++;
427                            $offset_counter++;
428                            // Stop if maximum of hits is reached
429
430                            if ($counter >= $this->getMaxHits()) {
431                                $this->limit_reached = true;
432                                $this->search_cache->setResults($this->results);
433                                return true;
434                            }
435                        }
436                    }
437                    continue;
438                }
439                $this->search_cache->appendToFailed($ref_id);
440            }
441        }
442        $this->search_cache->setResults($this->results);
443        return false;
444    }
445
446    /**
447     *
448     * Filter search area of result set
449     * @access	public
450     */
451    public function filterResults($a_root_node)
452    {
453        global $DIC;
454
455        $tree = $DIC['tree'];
456
457        $tmp_results = $this->getResults();
458        $this->results = array();
459        foreach ($tmp_results as $result) {
460            if ($tree->isGrandChild($a_root_node, $result['ref_id']) and $tree->isInTree($result['ref_id'])) {
461                $this->addResult($result['ref_id'], $result['obj_id'], $result['type']);
462                $this->__updateResultChilds($result['ref_id'], $result['child']);
463            }
464        }
465
466        return true;
467    }
468
469
470    /**
471     *
472     * Save search results
473     * @param integer DEFAULT_SEARCH or ADVANCED_SEARCH
474     * @access	public
475     */
476    public function save($a_type = DEFAULT_SEARCH)
477    {
478        $this->search_cache->save();
479        return false;
480    }
481    /**
482     *
483     * read search results
484     * @param integer DEFAULT_SEARCH or ADVANCED_SEARCH
485     * @access	public
486     */
487    public function read($a_type = DEFAULT_SEARCH)
488    {
489        $this->results = $this->search_cache->getResults();
490    }
491
492    // PRIVATE
493    /**
494     *
495     * Update childs for a specific entry
496     * @param integer object object_id
497     * @param array array of child ids. E.g 'pg', 'st'
498     * @access	private
499     */
500    public function __updateEntryChilds($a_obj_id, $a_childs)
501    {
502        if ($this->entries[$a_obj_id] and is_array($a_childs)) {
503            foreach ($a_childs as $child_id) {
504                if ($child_id) {
505                    $this->entries[$a_obj_id]['child'][$child_id] = $child_id;
506                }
507            }
508            return true;
509        }
510        return false;
511    }
512    /**
513     *
514     * Update childs for a specific result
515     * @param integer  object ref_id
516     * @param array array of child ids. E.g 'pg', 'st'
517     * @access	private
518     */
519    public function __updateResultChilds($a_ref_id, $a_childs)
520    {
521        if ($this->results[$a_ref_id] and is_array($a_childs)) {
522            foreach ($a_childs as $child_id) {
523                $this->results[$a_ref_id]['child'][$child_id] = $child_id;
524            }
525            return true;
526        }
527        return false;
528    }
529
530
531
532    public function __initSearchSettingsObject()
533    {
534        include_once 'Services/Search/classes/class.ilSearchSettings.php';
535
536        $this->search_settings = new ilSearchSettings();
537        if (!$this->preventOverwritingMaxhits()) {
538            $this->setMaxHits($this->search_settings->getMaxHits());
539        }
540    }
541
542    /**
543     * Init user search cache
544     *
545     * @access private
546     *
547     */
548    protected function initUserSearchCache()
549    {
550        include_once('Services/Search/classes/class.ilUserSearchCache.php');
551        $this->search_cache = ilUserSearchCache::_getInstance($this->getUserId());
552        $this->offset = $this->getMaxHits() * ($this->search_cache->getResultPageNumber() - 1) ;
553    }
554
555    /**
556     * If you call this function and pass "true" the maxhits setting will not be overwritten
557     * in __initSearchSettingsObject()
558     *
559     * @access	public
560     * @param	boolean	$a_flag	true or false to set the flag or leave blank to get the status of the flag
561     * @returmn	boolean	if called without parameter the status of the flag will be returned, otherwise $this
562     *
563     */
564    public function preventOverwritingMaxhits($a_flag = null)
565    {
566        if (null === $a_flag) {
567            return $this->preventOverwritingMaxhits;
568        }
569
570        $this->preventOverwritingMaxhits = $a_flag;
571
572        return $this;
573    }
574
575    /**
576     * The observer is used to call functions for filtering result.
577     * Every callback function should support the following parameters:
578     * array of ids. E.g: ref_id = 5,array(obj_id = 1,type = 'crs'),
579     * The function should return true or false.
580     * @param object class of callback function
581     * @param string name of callback method
582     * @access public
583     */
584    public function addObserver(&$a_class, $a_method)
585    {
586        $this->observers[] = array('class' => $a_class,
587                                   'method' => $a_method);
588        return true;
589    }
590    public function callListeners($a_ref_id, &$a_data)
591    {
592        foreach ($this->observers as $observer) {
593            $class = &$observer['class'];
594            $method = $observer['method'];
595
596            if (!$class->$method($a_ref_id, $a_data)) {
597                return false;
598            }
599        }
600        return true;
601    }
602} // END class.Search
603