MDL-11719 increased size of idnumber in user table - needed for some enrolment plugin...
[moodle.git] / search / querylib.php
1 <?php
2 /** 
3 * Global Search Engine for Moodle
4 *
5 * @package search
6 * @category core
7 * @subpackage search_engine
8 * @author Michael Champanis (mchampan) [cynnical@gmail.com], Valery Fremaux [valery.fremaux@club-internet.fr] > 1.8
9 * @date 2008/03/31
10 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
11 */
13 /**
14 * includes and requires
15 */
16 require_once("{$CFG->dirroot}/search/Zend/Search/Lucene.php");
18 define('DEFAULT_POPUP_SETTINGS', "\"menubar=0,location=0,scrollbars,resizable,width=600,height=450\"");
20 /**
21 * a class that represents a single result record of the search engine
22 */    
23 class SearchResult {
24 public  $url,
25         $title,
26         $doctype,
27         $author,
28         $score,
29         $number;
30
33 /**
34 * split this into Cache class and extend to SearchCache?
35 */
36 class SearchCache {
37 private $mode,
38         $valid;
40     // foresees other caching locations
41     public function __construct($mode = 'session') {
42         $accepted_modes = array('session');
44         if (in_array($mode, $accepted_modes)) {
45             $this->mode = $mode;
46         } else {
47             $this->mode = 'session';
48         } //else
50         $this->valid = true;
51     } 
53     /**
54     * returns the search cache status
55     * @return boolean
56     */
57     public function can_cache() {
58         return $this->valid;
59     } 
61     /**
62     *
63     *
64     */
65     public function cache($id = false, $object = false) {
66         //see if there was a previous query
67         $last_term = $this->fetch('search_last_term');
69         //if this query is different from the last, clear out the last one
70         if ($id != false and $last_term != $id) {
71             $this->clear($last_term);
72         } 
74         //store the new query if id and object are passed in
75         if ($object and $id) {
76             $this->store('search_last_term', $id);
77             $this->store($id, $object);
78             return true;
79         //otherwise return the stored results
80         } else if ($id and $this->exists($id)) {
81             return $this->fetch($id);
82         } 
83     } 
85     /**
86     * do key exist in cache ?
87     * @param id the object key
88     * @return boolean
89     */
90     private function exists($id) {
91         switch ($this->mode) {
92             case 'session' :
93             return isset($_SESSION[$id]);
94         } 
95     } 
97     /**
98     * clears a cached object in cache
99     * @param the object key to clear
100     * @return void
101     */
102     private function clear($id) {
103         switch ($this->mode) {
104             case 'session' :
105                 unset($_SESSION[$id]);
106                 session_unregister($id);
107             return;
108         } 
109     } 
111     /**
112     * fetches a cached object
113     * @param id the object identifier
114     * @return the object cached
115     */
116     private function fetch($id) {
117         switch ($this->mode) {
118             case 'session' :
119                 return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
120         } 
121     } 
123     /**
124     * put an object in cache
125     * @param id the key for that object
126     * @param object the object to cache as a serialized value
127     * @return void
128     */
129     private function store($id, $object) {
130         switch ($this->mode) {
131             case 'session' :
132                 $_SESSION[$id] = serialize($object);
133             return;
134         }
135     } 
136
138 /**
139 * Represents a single query with results
141 */
142 class SearchQuery {
143     private $index,
144             $term,
145             $pagenumber,
146             $cache,
147             $validquery,
148             $validindex,
149             $results,
150             $results_per_page,
151             $total_results;
153     /**
154     * constructor records query parameters
155     *
156     */
157     public function __construct($term = '', $page = 1, $results_per_page = 10, $cache = false) {
158         global $CFG;
160         $this->term       = $term;
161         $this->pagenumber = $page;
162         $this->cache      = $cache;
163         $this->validquery = true;
164         $this->validindex = true;
165         $this->results_per_page = $results_per_page;
167         $index_path = SEARCH_INDEX_PATH;
169         try {
170             $this->index = new Zend_Search_Lucene($index_path, false);
171         } catch(Exception $e) {
172             $this->validindex = false;
173             return;
174         } 
176         if (empty($this->term)) {
177             $this->validquery = false;
178         } else {
179             $this->set_query($this->term);
180         } 
181     } 
182     
183     /**
184     * determines state of query object depending on query entry and 
185     * tries to lauch search if all is OK
186     * @return void (this is only a state changing trigger).
187     */
188     public function set_query($term = '') {
189         if (!empty($term)) {
190             $this->term = $term;
191         }
193         if (empty($this->term)) {
194             $this->validquery = false;
195         } else {
196             $this->validquery = true;
197         }
199         if ($this->validquery and $this->validindex) {
200             $this->results = $this->get_results();
201         } else {
202             $this->results = array();
203         }
204     } 
206     /**
207     * accessor to the result table.
208     * @return an array of result records
209     */
210     public function results() {
211         return $this->results;
212     }
214     /**
215     * do the effective collection of results
216     * @param boolean $all
217     * @uses USER
218     */
219     private function process_results($all=false) {
220         global $USER;
222         $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
223         
224         //experimental - return more results
225         $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
226         $stripped_term = str_replace($strip_arr, '', $term);
228         $hits = $this->index->find($term." title:".$stripped_term." author:".$stripped_term);
229         //--
231         $hitcount = count($hits);
232         $this->total_results = $hitcount;
234         if ($hitcount == 0) return array();
236         $totalpages = ceil($hitcount/$this->results_per_page);
238         if (!$all) {
239             if ($hitcount < $this->results_per_page) {
240                 $this->pagenumber = 1;
241             } else if ($this->pagenumber > $totalpages) {
242                 $this->pagenumber = $totalpages;
243             }
245             $start = ($this->pagenumber - 1) * $this->results_per_page;
246             $end = $start + $this->results_per_page;
248             if ($end > $hitcount) {
249                 $end = $hitcount;
250             } 
251         } else {
252             $start = 0;
253             $end = $hitcount;
254         }
256         $resultdoc  = new SearchResult();
257         $resultdocs = array();
259         for ($i = $start; $i < $end; $i++) {
260             $hit = $hits[$i];
262             //check permissions on each result
263             if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id )) {
264                 $resultdoc->number  = $i;
265                 $resultdoc->url     = $hit->url;
266                 $resultdoc->title   = $hit->title;
267                 $resultdoc->score   = $hit->score;
268                 $resultdoc->doctype = $hit->doctype;
269                 $resultdoc->author  = $hit->author;
271                 //and store it
272                 $resultdocs[] = clone($resultdoc);
273             } else {
274                // lowers total_results one unit
275                $this->total_results--;
276             }
277         }
279         return $resultdocs;
280     }
282     /**
283     * get results of a search query using a caching strategy if available
284     * @return the result documents as an array of search objects
285     */
286     private function get_results() {
287         $cache = new SearchCache();
289         if ($this->cache and $cache->can_cache()) {
290             if (!($resultdocs = $cache->cache($this->term))) {
291                 $resultdocs = $this->process_results();
292                 //cache the results so we don't have to compute this on every page-load
293                 $cache->cache($this->term, $resultdocs);
294                 //print "Using new results.";
295             } else {
296             //There was something in the cache, so we're using that to save time
297             //print "Using cached results.";
298             } 
299         } else {
300             //no caching :(
301             //print "Caching disabled!";
302             $resultdocs = $this->process_results();
303         } 
304         return $resultdocs;
305     }
307     /**
308     * constructs the results paging links on results.
309     * @return string the results paging links
310     */
311     public function page_numbers() {
312       $pages  = $this->total_pages();
313       $query  = htmlentities($this->term);
314       $page   = $this->pagenumber;
315       $next   = get_string('next', 'search');
316       $back   = get_string('back', 'search');
318       $ret = "<div align='center' id='search_page_links'>";
320       //Back is disabled if we're on page 1
321       if ($page > 1) {
322         $ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>&lt; {$back}</a>&nbsp;";
323       } else {
324         $ret .= "&lt; {$back}&nbsp;";
325       } 
327       //don't <a href> the current page
328       for ($i = 1; $i <= $pages; $i++) {
329         if ($page == $i) {
330           $ret .= "($i)&nbsp;";
331         } else {
332           $ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a>&nbsp;";
333         } 
334       } 
336       //Next disabled if we're on the last page
337       if ($page < $pages) {
338         $ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} &gt;</a>&nbsp;";
339       } else {
340         $ret .= "{$next} &gt;&nbsp;";
341       } 
343       $ret .= "</div>";
345       //shorten really long page lists, to stop table distorting width-ways
346       if (strlen($ret) > 70) {
347         $start = 4;
348         $end = $page - 5;
349         $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
351         $start = $page + 5;
352         $end = $pages - 3;
353         $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
354       }
356       return $ret;
357     }
359     /**
360     * can the user see this result ?
361     * @param user a reference upon the user to be checked for access
362     * @param this_id the item identifier
363     * @param doctype the search document type. MAtches the module or block or 
364     * extra search source definition
365     * @param course_id the course reference of the searched result
366     * @param group_id the group identity attached to the found resource
367     * @param path the path that routes to the local lib.php of the searched 
368     * surrounding object fot that document
369     * @param item_type a subclassing information for complex module data models
370     * @uses CFG
371     * // TODO reorder parameters more consistently
372     */
373     private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id) {
374         global $CFG;
375        
376       /**
377       * course related checks
378       */
379       // admins can see everything, anyway.
380       if (isadmin()){
381         return true;
382       }
383             
384       // first check course compatibility against user : enrolled users to that course can see. 
385       $myCourses = get_my_courses($user->id);
386       $unenroled = !in_array($course_id, array_keys($myCourses));
387       
388       // if guests are allowed, logged guest can see
389       $isallowedguest = (isguest()) ? get_field('course', 'guest', 'id', $course_id) : false ;
390       
391       if ($unenroled && !$isallowedguest){
392          return false;
393       }
395       // if user is enrolled or is allowed user and course is hidden, can he see it ?
396       $visibility = get_field('course', 'visible', 'id', $course_id);
397       if ($visibility <= 0){
398           if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
399               return false;
400           }
401       }
403       /**
404       * prerecorded capabilities
405       */
406       // get context caching information and tries to discard unwanted records here
408       
409       /**
410       * final checks
411       */
412       // then give back indexing data to the module for local check
413       include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
414       $access_check_function = "{$doctype}_check_text_access";
415       
416       if (function_exists($access_check_function)){
417           $modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
418           // echo "module said $modulecheck for item $doctype/$item_type/$this_id";
419           return($modulecheck);
420       }
421         
422       return true;
423     }
425     /**
426     *
427     */
428     public function count() {
429       return $this->total_results;
430     } //count
432     /**
433     *
434     */
435     public function is_valid() {
436       return ($this->validquery and $this->validindex);
437     }
439     /**
440     *
441     */
442     public function is_valid_query() {
443       return $this->validquery;
444     }
446     /**
447     *
448     */
449     public function is_valid_index() {
450       return $this->validindex;
451     }
453     /**
454     *
455     */
456     public function total_pages() {
457       return ceil($this->count()/$this->results_per_page);
458     }
460     /**
461     *
462     */
463     public function get_pagenumber() {
464       return $this->pagenumber;
465     }
467     /**
468     *
469     */
470     public function get_results_per_page() {
471       return $this->results_per_page;
472     }
474 ?>